mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-14 09:23:09 +00:00
Merge branch 'develop' into variance-report-fix
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"chart_name": "Item-wise Annual Sales",
|
||||
"chart_type": "Report",
|
||||
"creation": "2020-07-20 20:17:16.474566",
|
||||
"custom_options": "",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"to_date\":\"frappe.datetime.nowdate()\"}",
|
||||
"filters_json": "{\"from_date\":\"2020-06-22\"}",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 14:42:25.512675",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Item-wise Annual Sales",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Item-wise Sales History",
|
||||
"timeseries": 0,
|
||||
"type": "Bar",
|
||||
"use_report_chart": 1,
|
||||
"y_axis": []
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"chart_name": "Sales Order Analysis",
|
||||
"chart_type": "Report",
|
||||
"creation": "2020-07-20 20:17:16.440393",
|
||||
"custom_options": "{\"type\": \"donut\", \"height\": 300, \"axisOptions\": {\"shortenYAxisNumbers\": 1}}",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"to_date\":\"frappe.datetime.nowdate()\"}",
|
||||
"filters_json": "{\"status\":[\"To Bill\",\"To Deliver\"],\"group_by_so\":0,\"from_date\":\"2020-06-22\"}",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 17:06:05.750660",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Analysis",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Sales Order Analysis",
|
||||
"timeseries": 0,
|
||||
"type": "Donut",
|
||||
"use_report_chart": 1,
|
||||
"y_axis": []
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"chart_name": "Sales Order Trends",
|
||||
"chart_type": "Report",
|
||||
"creation": "2020-07-20 20:17:16.508240",
|
||||
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
|
||||
"filters_json": "{\"period\":\"Monthly\",\"based_on\":\"Item\"}",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 16:24:45.726270",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Trends",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Sales Order Trends",
|
||||
"timeseries": 0,
|
||||
"type": "Line",
|
||||
"use_report_chart": 1,
|
||||
"y_axis": []
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"chart_name": "Top Customers",
|
||||
"chart_type": "Report",
|
||||
"creation": "2020-07-20 20:17:16.539281",
|
||||
"custom_options": "",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
|
||||
"filters_json": "{\"period\":\"Yearly\",\"based_on\":\"Customer\"}",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 17:03:10.320147",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Top Customers",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Delivery Note Trends",
|
||||
"timeseries": 0,
|
||||
"type": "Bar",
|
||||
"use_report_chart": 1,
|
||||
"y_axis": []
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
import json
|
||||
from frappe import _
|
||||
from frappe.utils import nowdate
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
def get_data():
|
||||
return frappe._dict({
|
||||
"dashboards": get_dashboards(),
|
||||
"charts": get_charts(),
|
||||
"number_cards": get_number_cards(),
|
||||
})
|
||||
|
||||
def get_company_for_dashboards():
|
||||
company = frappe.defaults.get_defaults().company
|
||||
if company:
|
||||
return company
|
||||
else:
|
||||
company_list = frappe.get_list("Company")
|
||||
if company_list:
|
||||
return company_list[0].name
|
||||
return None
|
||||
|
||||
company = frappe.get_doc("Company", get_company_for_dashboards())
|
||||
fiscal_year = get_fiscal_year(nowdate(), as_dict=1)
|
||||
fiscal_year_name = fiscal_year.get("name")
|
||||
start_date = str(fiscal_year.get("year_start_date"))
|
||||
end_date = str(fiscal_year.get("year_end_date"))
|
||||
|
||||
def get_dashboards():
|
||||
return [{
|
||||
"name": "Selling",
|
||||
"dashboard_name": "Selling",
|
||||
"charts": [
|
||||
{ "chart": "Sales Order Trends", "width": "Full"},
|
||||
{ "chart": "Top Customers", "width": "Half"},
|
||||
{ "chart": "Sales Order Analysis", "width": "Half"},
|
||||
{ "chart": "Item-wise Annual Sales", "width": "Full"}
|
||||
],
|
||||
"cards": [
|
||||
{ "card": "Annual Sales"},
|
||||
{ "card": "Sales Orders to Deliver"},
|
||||
{ "card": "Sales Orders to Bill"},
|
||||
{ "card": "Active Customers"}
|
||||
]
|
||||
}]
|
||||
|
||||
def get_charts():
|
||||
return [
|
||||
{
|
||||
"name": "Sales Order Analysis",
|
||||
"chart_name": _("Sales Order Analysis"),
|
||||
"chart_type": "Report",
|
||||
"custom_options": json.dumps({
|
||||
"type": "donut",
|
||||
"height": 300,
|
||||
"axisOptions": {"shortenYAxisNumbers": 1}
|
||||
}),
|
||||
"doctype": "Dashboard Chart",
|
||||
"filters_json": json.dumps({
|
||||
"company": company.name,
|
||||
"from_date": start_date,
|
||||
"to_date": end_date
|
||||
}),
|
||||
"is_custom": 1,
|
||||
"is_public": 1,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Sales Order Analysis",
|
||||
"type": "Donut"
|
||||
},
|
||||
{
|
||||
"name": "Item-wise Annual Sales",
|
||||
"chart_name": _("Item-wise Annual Sales"),
|
||||
"chart_type": "Report",
|
||||
"doctype": "Dashboard Chart",
|
||||
"filters_json": json.dumps({
|
||||
"company": company.name,
|
||||
"from_date": start_date,
|
||||
"to_date": end_date
|
||||
}),
|
||||
"is_custom": 1,
|
||||
"is_public": 1,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Item-wise Sales History",
|
||||
"type": "Bar"
|
||||
},
|
||||
{
|
||||
"name": "Sales Order Trends",
|
||||
"chart_name": _("Sales Order Trends"),
|
||||
"chart_type": "Report",
|
||||
"custom_options": json.dumps({
|
||||
"type": "line",
|
||||
"axisOptions": {"shortenYAxisNumbers": 1},
|
||||
"tooltipOptions": {},
|
||||
"lineOptions": {
|
||||
"regionFill": 1
|
||||
}
|
||||
}),
|
||||
"doctype": "Dashboard Chart",
|
||||
"filters_json": json.dumps({
|
||||
"company": company.name,
|
||||
"period": "Monthly",
|
||||
"fiscal_year": fiscal_year_name,
|
||||
"based_on": "Item"
|
||||
}),
|
||||
"is_custom": 1,
|
||||
"is_public": 1,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Sales Order Trends",
|
||||
"type": "Line"
|
||||
},
|
||||
{
|
||||
"name": "Top Customers",
|
||||
"chart_name": _("Top Customers"),
|
||||
"chart_type": "Report",
|
||||
"doctype": "Dashboard Chart",
|
||||
"filters_json": json.dumps({
|
||||
"company": company.name,
|
||||
"period": "Monthly",
|
||||
"fiscal_year": fiscal_year_name,
|
||||
"based_on": "Customer"
|
||||
}),
|
||||
"is_custom": 1,
|
||||
"is_public": 1,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Delivery Note Trends",
|
||||
"type": "Bar"
|
||||
}
|
||||
]
|
||||
|
||||
def get_number_cards():
|
||||
return [
|
||||
{
|
||||
"name": "Annual Sales",
|
||||
"aggregate_function_based_on": "base_net_total",
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Order",
|
||||
"filters_json": json.dumps([
|
||||
["Sales Order", "transaction_date", "Between", [start_date, end_date], False],
|
||||
["Sales Order", "status", "not in", ["Draft", "Cancelled", "Closed", None], False],
|
||||
["Sales Order", "docstatus", "=", 1, False],
|
||||
["Sales Order", "company", "=", company.name, False]
|
||||
]),
|
||||
"function": "Sum",
|
||||
"is_public": 1,
|
||||
"label": _("Annual Sales"),
|
||||
"owner": "Administrator",
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Monthly"
|
||||
},
|
||||
{
|
||||
"name": "Sales Orders to Deliver",
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Order",
|
||||
"filters_json": json.dumps([
|
||||
["Sales Order", "status", "in", ["To Deliver and Bill", "To Deliver", None], False],
|
||||
["Sales Order", "docstatus", "=", 1, False],
|
||||
["Sales Order", "company", "=", company.name, False]
|
||||
]),
|
||||
"function": "Count",
|
||||
"is_public": 1,
|
||||
"label": _("Sales Orders to Deliver"),
|
||||
"owner": "Administrator",
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Weekly"
|
||||
},
|
||||
{
|
||||
"name": "Sales Orders to Bill",
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Order",
|
||||
"filters_json": json.dumps([
|
||||
["Sales Order", "status", "in", ["To Deliver and Bill", "To Bill", None], False],
|
||||
["Sales Order", "docstatus", "=", 1, False],
|
||||
["Sales Order", "company", "=", company.name, False]
|
||||
]),
|
||||
"function": "Count",
|
||||
"is_public": 1,
|
||||
"label": _("Sales Orders to Bill"),
|
||||
"owner": "Administrator",
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Weekly"
|
||||
},
|
||||
{
|
||||
"name": "Active Customers",
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Customer",
|
||||
"filters_json": json.dumps([["Customer", "disabled", "=", "0"]]),
|
||||
"function": "Count",
|
||||
"is_public": 1,
|
||||
"label": "Active Customers",
|
||||
"owner": "Administrator",
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Monthly"
|
||||
}
|
||||
]
|
||||
@@ -3,7 +3,7 @@
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Retail Operations",
|
||||
"links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"POS\",\n \"name\": \"pos\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]"
|
||||
"links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point of Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"Point of Sale\",\n \"name\": \"point-of-sale\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]"
|
||||
}
|
||||
],
|
||||
"category": "Domains",
|
||||
@@ -14,10 +14,11 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desk Page",
|
||||
"extends_another_page": 0,
|
||||
"hide_custom": 0,
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Retail",
|
||||
"modified": "2020-04-26 22:42:39.346750",
|
||||
"modified": "2020-08-20 18:00:07.515691",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Retail",
|
||||
@@ -25,5 +26,27 @@
|
||||
"pin_to_bottom": 0,
|
||||
"pin_to_top": 0,
|
||||
"restrict_to_domain": "Retail",
|
||||
"shortcuts": []
|
||||
"shortcuts": [
|
||||
{
|
||||
"color": "#9deca2",
|
||||
"doc_view": "",
|
||||
"format": "{} Active",
|
||||
"label": "Point of Sale Profile",
|
||||
"link_to": "POS Profile",
|
||||
"stats_filter": "{\n \"disabled\": 0\n}",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"doc_view": "",
|
||||
"label": "Point of Sale",
|
||||
"link_to": "point-of-sale",
|
||||
"type": "Page"
|
||||
},
|
||||
{
|
||||
"doc_view": "",
|
||||
"label": "POS Settings",
|
||||
"link_to": "POS Settings",
|
||||
"type": "DocType"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Key Reports",
|
||||
"links": "[\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]"
|
||||
"links": "[\n {\n \n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
@@ -44,7 +44,7 @@
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Selling",
|
||||
"modified": "2020-06-29 19:26:35.139097",
|
||||
"modified": "2020-08-15 10:12:53.131621",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling",
|
||||
|
||||
@@ -184,6 +184,14 @@ class Customer(TransactionBase):
|
||||
def validate_credit_limit_on_change(self):
|
||||
if self.get("__islocal") or not self.credit_limits:
|
||||
return
|
||||
|
||||
past_credit_limits = [d.credit_limit
|
||||
for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")]
|
||||
|
||||
current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)]
|
||||
|
||||
if past_credit_limits == current_credit_limits:
|
||||
return
|
||||
|
||||
company_record = []
|
||||
for limit in self.credit_limits:
|
||||
@@ -340,6 +348,7 @@ def get_loyalty_programs(doc):
|
||||
return lp_details
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None):
|
||||
from erpnext.controllers.queries import get_fields
|
||||
fields = ["name", "customer_name", "customer_group", "territory"]
|
||||
@@ -542,6 +551,7 @@ def make_address(args, is_primary_address=1):
|
||||
return address
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
||||
customer = filters.get('customer')
|
||||
return frappe.db.sql("""
|
||||
|
||||
@@ -12,7 +12,8 @@ def get_data():
|
||||
'Payment Entry': 'party',
|
||||
'Quotation': 'party_name',
|
||||
'Opportunity': 'party_name',
|
||||
'Bank Account': 'party'
|
||||
'Bank Account': 'party',
|
||||
'Subscription': 'party'
|
||||
},
|
||||
'dynamic_links': {
|
||||
'party_name': ['Customer', 'quotation_to']
|
||||
@@ -32,7 +33,7 @@ def get_data():
|
||||
},
|
||||
{
|
||||
'label': _('Support'),
|
||||
'items': ['Issue']
|
||||
'items': ['Issue', 'Maintenance Visit', 'Installation Note', 'Warranty Claim']
|
||||
},
|
||||
{
|
||||
'label': _('Projects'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:source_name",
|
||||
"beta": 0,
|
||||
"creation": "2016-09-16 01:47:47.382372",
|
||||
@@ -74,7 +74,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-09-16 02:03:01.441622",
|
||||
"modified": "2020-09-16 02:03:01.441622",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Lead Source",
|
||||
@@ -128,4 +128,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<div class="clearfix"></div>
|
||||
<div class="box">
|
||||
<div class="grid-body">
|
||||
<div class="rows text-center">
|
||||
|
||||
<!-- Sales summary section -->
|
||||
<div>
|
||||
<h6 class="text-center uppercase" style="color: #8D99A6">{{ _("Sales Summary") }}</h6>
|
||||
<div class="tax-break-up" style="overflow-x: auto;">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-left">{{ _('Grand Total') }}</td>
|
||||
<td class='text-right'>{{ data.grand_total or '' }} {{ currency.symbol }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left">{{ _('Net Total') }}</td>
|
||||
<td class='text-right'>{{ data.net_total or '' }} {{ currency.symbol }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left">{{ _('Total Quantity') }}</td>
|
||||
<td class='text-right'>{{ data.total_quantity or '' }}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Section end -->
|
||||
|
||||
<!-- Mode of payment section -->
|
||||
<div>
|
||||
<h6 class="text-center uppercase" style="color: #8D99A6">{{ _("Mode of Payments") }}</h6>
|
||||
<div class="tax-break-up" style="overflow-x: auto;">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ _("Mode of Payment") }}</th>
|
||||
<th class="text-right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in data.payment_reconciliation %}
|
||||
<tr>
|
||||
<td class="text-left">{{ d.mode_of_payment }}</td>
|
||||
<td class='text-right'>{{ d.expected_amount }} {{ currency.symbol }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Section end -->
|
||||
|
||||
<!-- Taxes section -->
|
||||
<div>
|
||||
<h6 class="text-center uppercase" style="color: #8D99A6">{{ _("Taxes") }}</h6>
|
||||
<div class="tax-break-up" style="overflow-x: auto;">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ _("Rate") }}</th>
|
||||
<th class="text-right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in data.taxes %}
|
||||
<tr>
|
||||
<td class="text-left">{{ d.rate }} %</td>
|
||||
<td class='text-right'>{{ d.amount }} {{ currency.symbol }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Section end -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('POS Closing Voucher', {
|
||||
onload: function(frm) {
|
||||
frm.set_query("pos_profile", function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
'user': doc.user
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("user", function(doc) {
|
||||
return {
|
||||
query: "erpnext.selling.doctype.pos_closing_voucher.pos_closing_voucher.get_cashiers",
|
||||
filters: {
|
||||
'parent': doc.pos_profile
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
total_amount: function(frm) {
|
||||
get_difference_amount(frm);
|
||||
},
|
||||
custody_amount: function(frm){
|
||||
get_difference_amount(frm);
|
||||
},
|
||||
expense_amount: function(frm){
|
||||
get_difference_amount(frm);
|
||||
},
|
||||
refresh: function(frm) {
|
||||
get_closing_voucher_details(frm);
|
||||
},
|
||||
period_start_date: function(frm) {
|
||||
get_closing_voucher_details(frm);
|
||||
},
|
||||
period_end_date: function(frm) {
|
||||
get_closing_voucher_details(frm);
|
||||
},
|
||||
company: function(frm) {
|
||||
get_closing_voucher_details(frm);
|
||||
},
|
||||
pos_profile: function(frm) {
|
||||
get_closing_voucher_details(frm);
|
||||
},
|
||||
user: function(frm) {
|
||||
get_closing_voucher_details(frm);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on('POS Closing Voucher Details', {
|
||||
collected_amount: function(doc, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "difference", row.collected_amount - row.expected_amount);
|
||||
}
|
||||
});
|
||||
|
||||
var get_difference_amount = function(frm){
|
||||
frm.doc.difference = frm.doc.total_amount - frm.doc.custody_amount - frm.doc.expense_amount;
|
||||
refresh_field("difference");
|
||||
};
|
||||
|
||||
var get_closing_voucher_details = function(frm) {
|
||||
if (frm.doc.period_end_date && frm.doc.period_start_date && frm.doc.company && frm.doc.pos_profile && frm.doc.user) {
|
||||
frappe.call({
|
||||
method: "get_closing_voucher_details",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
refresh_field("payment_reconciliation");
|
||||
refresh_field("sales_invoices_summary");
|
||||
refresh_field("taxes");
|
||||
|
||||
refresh_field("grand_total");
|
||||
refresh_field("net_total");
|
||||
refresh_field("total_quantity");
|
||||
refresh_field("total_amount");
|
||||
|
||||
frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from collections import defaultdict
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||
import json
|
||||
|
||||
class POSClosingVoucher(Document):
|
||||
def get_closing_voucher_details(self):
|
||||
filters = {
|
||||
'doc': self.name,
|
||||
'from_date': self.period_start_date,
|
||||
'to_date': self.period_end_date,
|
||||
'company': self.company,
|
||||
'pos_profile': self.pos_profile,
|
||||
'user': self.user,
|
||||
'is_pos': 1
|
||||
}
|
||||
|
||||
invoice_list = get_invoices(filters)
|
||||
self.set_invoice_list(invoice_list)
|
||||
|
||||
sales_summary = get_sales_summary(invoice_list)
|
||||
self.set_sales_summary_values(sales_summary)
|
||||
self.total_amount = sales_summary['grand_total']
|
||||
|
||||
if not self.get('payment_reconciliation'):
|
||||
mop = get_mode_of_payment_details(invoice_list)
|
||||
self.set_mode_of_payments(mop)
|
||||
|
||||
taxes = get_tax_details(invoice_list)
|
||||
self.set_taxes(taxes)
|
||||
|
||||
return self.get_payment_reconciliation_details()
|
||||
|
||||
def validate(self):
|
||||
user = frappe.get_all('POS Closing Voucher',
|
||||
filters = {
|
||||
'user': self.user,
|
||||
'docstatus': 1
|
||||
},
|
||||
or_filters = {
|
||||
'period_start_date': ('between', [self.period_start_date, self.period_end_date]),
|
||||
'period_end_date': ('between', [self.period_start_date, self.period_end_date])
|
||||
})
|
||||
|
||||
if user:
|
||||
frappe.throw(_("POS Closing Voucher alreday exists for {0} between date {1} and {2}")
|
||||
.format(self.user, self.period_start_date, self.period_end_date))
|
||||
|
||||
def set_invoice_list(self, invoice_list):
|
||||
self.sales_invoices_summary = []
|
||||
for invoice in invoice_list:
|
||||
self.append('sales_invoices_summary', {
|
||||
'invoice': invoice['name'],
|
||||
'qty_of_items': invoice['pos_total_qty'],
|
||||
'grand_total': invoice['grand_total']
|
||||
})
|
||||
|
||||
def set_sales_summary_values(self, sales_summary):
|
||||
self.grand_total = sales_summary['grand_total']
|
||||
self.net_total = sales_summary['net_total']
|
||||
self.total_quantity = sales_summary['total_qty']
|
||||
|
||||
def set_mode_of_payments(self, mop):
|
||||
self.payment_reconciliation = []
|
||||
for m in mop:
|
||||
self.append('payment_reconciliation', {
|
||||
'mode_of_payment': m['name'],
|
||||
'expected_amount': m['amount']
|
||||
})
|
||||
|
||||
def set_taxes(self, taxes):
|
||||
self.taxes = []
|
||||
for tax in taxes:
|
||||
self.append('taxes', {
|
||||
'rate': tax['rate'],
|
||||
'amount': tax['amount']
|
||||
})
|
||||
|
||||
def get_payment_reconciliation_details(self):
|
||||
currency = get_company_currency(self)
|
||||
return frappe.render_template("erpnext/selling/doctype/pos_closing_voucher/closing_voucher_details.html",
|
||||
{"data": self, "currency": currency})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
|
||||
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'])
|
||||
cashiers = [cashier for cashier in set(c['user'] for c in cashiers_list)]
|
||||
return [[c] for c in cashiers]
|
||||
|
||||
def get_mode_of_payment_details(invoice_list):
|
||||
mode_of_payment_details = []
|
||||
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
|
||||
if invoice_list:
|
||||
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
|
||||
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
|
||||
from `tabSales Invoice` a, `tabSales Invoice Payment` b
|
||||
where a.name = b.parent
|
||||
and a.name in ({invoice_list_names})
|
||||
group by a.owner, a.posting_date, mode_of_payment
|
||||
union
|
||||
select a.owner,a.posting_date,
|
||||
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_paid_amount) as paid_amount
|
||||
from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c
|
||||
where a.name = c.reference_name
|
||||
and b.name = c.parent
|
||||
and a.name in ({invoice_list_names})
|
||||
group by a.owner, a.posting_date, mode_of_payment
|
||||
union
|
||||
select a.owner, a.posting_date,
|
||||
ifnull(a.voucher_type,'') as mode_of_payment, sum(b.credit)
|
||||
from `tabJournal Entry` a, `tabJournal Entry Account` b
|
||||
where a.name = b.parent
|
||||
and a.docstatus = 1
|
||||
and b.reference_type = "Sales Invoice"
|
||||
and b.reference_name in ({invoice_list_names})
|
||||
group by a.owner, a.posting_date, mode_of_payment
|
||||
""".format(invoice_list_names=invoice_list_names), as_dict=1)
|
||||
|
||||
inv_change_amount = frappe.db.sql("""select a.owner, a.posting_date,
|
||||
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(a.base_change_amount) as change_amount
|
||||
from `tabSales Invoice` a, `tabSales Invoice Payment` b
|
||||
where a.name = b.parent
|
||||
and a.name in ({invoice_list_names})
|
||||
and b.mode_of_payment = 'Cash'
|
||||
and a.base_change_amount > 0
|
||||
group by a.owner, a.posting_date, mode_of_payment""".format(invoice_list_names=invoice_list_names), as_dict=1)
|
||||
|
||||
for d in inv_change_amount:
|
||||
for det in inv_mop_detail:
|
||||
if det["owner"] == d["owner"] and det["posting_date"] == d["posting_date"] and det["mode_of_payment"] == d["mode_of_payment"]:
|
||||
paid_amount = det["paid_amount"] - d["change_amount"]
|
||||
det["paid_amount"] = paid_amount
|
||||
|
||||
payment_details = defaultdict(int)
|
||||
for d in inv_mop_detail:
|
||||
payment_details[d.mode_of_payment] += d.paid_amount
|
||||
|
||||
for m in payment_details:
|
||||
mode_of_payment_details.append({'name': m, 'amount': payment_details[m]})
|
||||
|
||||
return mode_of_payment_details
|
||||
|
||||
def get_tax_details(invoice_list):
|
||||
tax_breakup = []
|
||||
tax_details = defaultdict(int)
|
||||
for invoice in invoice_list:
|
||||
doc = frappe.get_doc("Sales Invoice", invoice.name)
|
||||
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(doc)
|
||||
|
||||
if itemised_tax:
|
||||
for a in itemised_tax:
|
||||
for b in itemised_tax[a]:
|
||||
for c in itemised_tax[a][b]:
|
||||
if c == 'tax_rate':
|
||||
tax_details[itemised_tax[a][b][c]] += itemised_tax[a][b]['tax_amount']
|
||||
|
||||
for t in tax_details:
|
||||
tax_breakup.append({'rate': t, 'amount': tax_details[t]})
|
||||
|
||||
return tax_breakup
|
||||
|
||||
def get_sales_summary(invoice_list):
|
||||
net_total = sum(item['net_total'] for item in invoice_list)
|
||||
grand_total = sum(item['grand_total'] for item in invoice_list)
|
||||
total_qty = sum(item['pos_total_qty'] for item in invoice_list)
|
||||
|
||||
return {'net_total': net_total, 'grand_total': grand_total, 'total_qty': total_qty}
|
||||
|
||||
def get_company_currency(doc):
|
||||
currency = frappe.get_cached_value('Company', doc.company, "default_currency")
|
||||
return frappe.get_doc('Currency', currency)
|
||||
|
||||
def get_invoices(filters):
|
||||
return frappe.db.sql("""select a.name, a.base_grand_total as grand_total,
|
||||
a.base_net_total as net_total, a.pos_total_qty
|
||||
from `tabSales Invoice` a
|
||||
where a.docstatus = 1 and a.posting_date >= %(from_date)s
|
||||
and a.posting_date <= %(to_date)s and a.company=%(company)s
|
||||
and a.pos_profile = %(pos_profile)s and a.is_pos = %(is_pos)s
|
||||
and a.owner = %(user)s""",
|
||||
filters, as_dict=1)
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// rename this file from _test_[name] to test_[name] to activate
|
||||
// and remove above this line
|
||||
|
||||
QUnit.test("test: POS Closing Voucher", function (assert) {
|
||||
let done = assert.async();
|
||||
|
||||
// number of asserts
|
||||
assert.expect(1);
|
||||
|
||||
frappe.run_serially([
|
||||
// insert a new POS Closing Voucher
|
||||
() => frappe.tests.make('POS Closing Voucher', [
|
||||
// values to be set
|
||||
{key: 'value'}
|
||||
]),
|
||||
() => {
|
||||
assert.equal(cur_frm.doc.key, 'value');
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.utils import nowdate
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
|
||||
class TestPOSClosingVoucher(unittest.TestCase):
|
||||
def test_pos_closing_voucher(self):
|
||||
old_user = frappe.session.user
|
||||
user = 'test@example.com'
|
||||
test_user = frappe.get_doc('User', user)
|
||||
|
||||
roles = ("Accounts Manager", "Accounts User", "Sales Manager")
|
||||
test_user.add_roles(*roles)
|
||||
frappe.set_user(user)
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.append('applicable_for_users', {
|
||||
'default': 1,
|
||||
'user': user
|
||||
})
|
||||
|
||||
pos_profile.save()
|
||||
|
||||
si1 = create_sales_invoice(is_pos=1, rate=3500, do_not_submit=1)
|
||||
si1.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
|
||||
})
|
||||
si1.submit()
|
||||
|
||||
si2 = create_sales_invoice(is_pos=1, rate=3200, do_not_submit=1)
|
||||
si2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
|
||||
})
|
||||
si2.submit()
|
||||
|
||||
pcv_doc = create_pos_closing_voucher(user=user,
|
||||
pos_profile=pos_profile.name, collected_amount=6700)
|
||||
|
||||
pcv_doc.get_closing_voucher_details()
|
||||
|
||||
self.assertEqual(pcv_doc.total_quantity, 2)
|
||||
self.assertEqual(pcv_doc.net_total, 6700)
|
||||
|
||||
payment = pcv_doc.payment_reconciliation[0]
|
||||
self.assertEqual(payment.mode_of_payment, 'Cash')
|
||||
|
||||
si1.load_from_db()
|
||||
si1.cancel()
|
||||
|
||||
si2.load_from_db()
|
||||
si2.cancel()
|
||||
|
||||
test_user.load_from_db()
|
||||
test_user.remove_roles(*roles)
|
||||
|
||||
frappe.set_user(old_user)
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def create_pos_closing_voucher(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'POS Closing Voucher',
|
||||
'period_start_date': args.period_start_date or nowdate(),
|
||||
'period_end_date': args.period_end_date or nowdate(),
|
||||
'posting_date': args.posting_date or nowdate(),
|
||||
'company': args.company or "_Test Company",
|
||||
'pos_profile': args.pos_profile,
|
||||
'user': args.user or "Administrator",
|
||||
})
|
||||
|
||||
doc.get_closing_voucher_details()
|
||||
if doc.get('payment_reconciliation'):
|
||||
doc.payment_reconciliation[0].collected_amount = (args.collected_amount or
|
||||
doc.payment_reconciliation[0].expected_amount)
|
||||
|
||||
doc.save()
|
||||
return doc
|
||||
@@ -1,172 +0,0 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-05-28 19:10:47.580174",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "mode_of_payment",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Mode of Payment",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Mode of Payment",
|
||||
"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,
|
||||
"default": "0.0",
|
||||
"fieldname": "collected_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Collected Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "currency",
|
||||
"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": "expected_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Expected Amount",
|
||||
"length": 0,
|
||||
"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": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "difference",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Difference",
|
||||
"length": 0,
|
||||
"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
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-29 17:47:16.311557",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "POS Closing Voucher Details",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
|
||||
class POSClosingVoucherDetails(Document):
|
||||
pass
|
||||
@@ -1,138 +0,0 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-05-29 14:50:08.687453",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Invoices",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Sales Invoice",
|
||||
"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": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "qty_of_items",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Quantity of Items",
|
||||
"length": 0,
|
||||
"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": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Grand Total",
|
||||
"length": 0,
|
||||
"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
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-29 17:46:46.539993",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "POS Closing Voucher Invoices",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
|
||||
class POSClosingVoucherInvoices(Document):
|
||||
pass
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-05-30 09:11:22.535470",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Rate",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"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": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Amount",
|
||||
"length": 0,
|
||||
"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
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-30 09:11:22.535470",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "POS Closing Voucher Taxes",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
|
||||
class POSClosingVoucherTaxes(Document):
|
||||
pass
|
||||
@@ -29,6 +29,7 @@ class ProductBundle(Document):
|
||||
frappe.throw(_("Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save").format(item.idx, frappe.bold(item.item_code)))
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
|
||||
|
||||
@@ -654,6 +654,7 @@
|
||||
"fieldname": "base_in_words",
|
||||
"fieldtype": "Data",
|
||||
"label": "In Words (Company Currency)",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
@@ -713,6 +714,7 @@
|
||||
"fieldname": "in_words",
|
||||
"fieldtype": "Data",
|
||||
"label": "In Words",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words_export",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
@@ -921,7 +923,7 @@
|
||||
"fieldname": "lost_reasons",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Lost Reasons",
|
||||
"options": "Lost Reason Detail",
|
||||
"options": "Quotation Lost Reason Detail",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
@@ -930,7 +932,7 @@
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"max_attachments": 1,
|
||||
"modified": "2019-12-30 19:14:56.630270",
|
||||
"modified": "2020-07-26 17:46:19.951223",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
||||
@@ -68,7 +68,7 @@ class Quotation(SellingController):
|
||||
|
||||
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
|
||||
if not self.has_sales_order():
|
||||
get_lost_reasons = frappe.get_list('Opportunity Lost Reason',
|
||||
get_lost_reasons = frappe.get_list('Quotation Lost Reason',
|
||||
fields = ["name"])
|
||||
lost_reasons_lst = [reason.get('name') for reason in get_lost_reasons]
|
||||
frappe.db.set(self, 'status', 'Lost')
|
||||
@@ -285,9 +285,17 @@ def _make_customer(source_name, ignore_permissions=False):
|
||||
return customer
|
||||
else:
|
||||
raise
|
||||
except frappe.MandatoryError:
|
||||
except frappe.MandatoryError as e:
|
||||
mandatory_fields = e.args[0].split(':')[1].split(',')
|
||||
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
|
||||
|
||||
frappe.local.message_log = []
|
||||
frappe.throw(_("Please create Customer from Lead {0}").format(lead_name))
|
||||
lead_link = frappe.utils.get_link_to_form("Lead", lead_name)
|
||||
message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
|
||||
message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
|
||||
message += _("Please create Customer from Lead {0}.").format(lead_link)
|
||||
|
||||
frappe.throw(message, title=_("Mandatory Missing"))
|
||||
else:
|
||||
return customer_name
|
||||
else:
|
||||
|
||||
@@ -928,6 +928,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "In Words (Company Currency)",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
@@ -986,6 +987,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "In Words",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words_export",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
@@ -1458,7 +1460,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-30 11:56:42.301317",
|
||||
"modified": "2020-07-31 14:13:17.962015",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
@@ -1532,7 +1534,7 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"title_field": "customer",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
@@ -888,6 +888,7 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier(doctype, txt, searchfield, start, page_len, filters):
|
||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
||||
if supp_master_name == "Supplier Name":
|
||||
|
||||
@@ -10,6 +10,7 @@ def get_data():
|
||||
'Payment Entry': 'reference_name',
|
||||
'Payment Request': 'reference_name',
|
||||
'Auto Repeat': 'reference_document',
|
||||
'Maintenance Visit': 'prevdoc_docname'
|
||||
},
|
||||
'internal_links': {
|
||||
'Quotation': ['items', 'prevdoc_docname']
|
||||
@@ -17,7 +18,7 @@ def get_data():
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Fulfillment'),
|
||||
'items': ['Sales Invoice', 'Pick List', 'Delivery Note']
|
||||
'items': ['Sales Invoice', 'Pick List', 'Delivery Note', 'Maintenance Visit']
|
||||
},
|
||||
{
|
||||
'label': _('Purchasing'),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"creation": "2020-07-20 20:17:16.653866",
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Customer",
|
||||
"dynamic_filters_json": "",
|
||||
"filters_json": "[[\"Customer\",\"disabled\",\"=\",\"0\"]]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Active Customers",
|
||||
"modified": "2020-07-22 14:20:32.268103",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Active Customers",
|
||||
"owner": "Administrator",
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Monthly",
|
||||
"type": "Document Type"
|
||||
}
|
||||
22
erpnext/selling/number_card/annual_sales/annual_sales.json
Normal file
22
erpnext/selling/number_card/annual_sales/annual_sales.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"aggregate_function_based_on": "base_net_total",
|
||||
"creation": "2020-07-20 20:17:16.568132",
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Order",
|
||||
"dynamic_filters_json": "[[\"Sales Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Sales Order\",\"status\",\"not in\",[\"Draft\",\"Cancelled\",\"Closed\",null],false],[\"Sales Order\",\"docstatus\",\"=\",\"1\",false],[\"Sales Order\",\"modified\",\"Timespan\",\"this year\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Annual Sales",
|
||||
"modified": "2020-07-22 16:56:33.747156",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Annual Sales",
|
||||
"owner": "Administrator",
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Monthly",
|
||||
"type": "Document Type"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"creation": "2020-07-20 20:17:16.625001",
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Order",
|
||||
"dynamic_filters_json": "[[\"Sales Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Sales Order\",\"status\",\"in\",[\"To Deliver and Bill\",\"To Bill\",null],false],[\"Sales Order\",\"docstatus\",\"=\",\"1\",false]]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Sales Orders to Bill",
|
||||
"modified": "2020-07-22 14:20:09.918626",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Orders to Bill",
|
||||
"owner": "Administrator",
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Weekly",
|
||||
"type": "Document Type"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"creation": "2020-07-20 20:17:16.596857",
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Order",
|
||||
"dynamic_filters_json": "[[\"Sales Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Sales Order\",\"status\",\"in\",[\"To Deliver and Bill\",\"To Deliver\",null],false],[\"Sales Order\",\"docstatus\",\"=\",\"1\",false]]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Sales Orders to Deliver",
|
||||
"modified": "2020-07-22 14:19:28.833784",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Orders to Deliver",
|
||||
"owner": "Administrator",
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Weekly",
|
||||
"type": "Document Type"
|
||||
}
|
||||
1
erpnext/selling/page/point_of_sale/onscan.js
Normal file
1
erpnext/selling/page/point_of_sale/onscan.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,33 @@
|
||||
{
|
||||
"content": null,
|
||||
"creation": "2017-08-07 17:08:56.737947",
|
||||
"docstatus": 0,
|
||||
"doctype": "Page",
|
||||
"idx": 0,
|
||||
"modified": "2017-09-11 13:49:05.415211",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "point-of-sale",
|
||||
"owner": "Administrator",
|
||||
"page_name": "Point of Sale",
|
||||
"restrict_to_domain": "Retail",
|
||||
"content": null,
|
||||
"creation": "2020-01-28 22:05:44.819140",
|
||||
"docstatus": 0,
|
||||
"doctype": "Page",
|
||||
"idx": 0,
|
||||
"modified": "2020-06-01 15:41:06.348380",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "point-of-sale",
|
||||
"owner": "Administrator",
|
||||
"page_name": "Point of Sale",
|
||||
"restrict_to_domain": "Retail",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
}
|
||||
],
|
||||
"script": null,
|
||||
"standard": "Yes",
|
||||
"style": null,
|
||||
"system_page": 0,
|
||||
"title": "Point of Sale"
|
||||
],
|
||||
"script": null,
|
||||
"standard": "Yes",
|
||||
"style": null,
|
||||
"system_page": 0,
|
||||
"title": "Point Of Sale"
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import frappe, json
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
from frappe.utils import cint
|
||||
from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
|
||||
|
||||
from six import string_types
|
||||
|
||||
@@ -13,10 +14,9 @@ from six import string_types
|
||||
def get_items(start, page_length, price_list, item_group, search_value="", pos_profile=None):
|
||||
data = dict()
|
||||
warehouse = ""
|
||||
display_items_in_stock = 0
|
||||
|
||||
if pos_profile:
|
||||
warehouse, display_items_in_stock = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'display_items_in_stock'])
|
||||
warehouse = frappe.db.get_value('POS Profile', pos_profile, ['warehouse'])
|
||||
|
||||
if not frappe.db.exists('Item Group', item_group):
|
||||
item_group = get_root_of('Item Group')
|
||||
@@ -43,6 +43,7 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
||||
SELECT
|
||||
name AS item_code,
|
||||
item_name,
|
||||
description,
|
||||
stock_uom,
|
||||
image AS item_image,
|
||||
idx AS idx,
|
||||
@@ -53,10 +54,11 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
||||
disabled = 0
|
||||
AND has_variants = 0
|
||||
AND is_sales_item = 1
|
||||
AND is_fixed_asset = 0
|
||||
AND item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
|
||||
AND {condition}
|
||||
ORDER BY
|
||||
idx desc
|
||||
name asc
|
||||
LIMIT
|
||||
{start}, {page_length}"""
|
||||
.format(
|
||||
@@ -73,34 +75,16 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
||||
fields = ["item_code", "price_list_rate", "currency"],
|
||||
filters = {'price_list': price_list, 'item_code': ['in', items]})
|
||||
|
||||
item_prices, bin_data = {}, {}
|
||||
item_prices = {}
|
||||
for d in item_prices_data:
|
||||
item_prices[d.item_code] = d
|
||||
|
||||
# prepare filter for bin query
|
||||
bin_filters = {'item_code': ['in', items]}
|
||||
if warehouse:
|
||||
bin_filters['warehouse'] = warehouse
|
||||
if display_items_in_stock:
|
||||
bin_filters['actual_qty'] = [">", 0]
|
||||
|
||||
# query item bin
|
||||
bin_data = frappe.get_all(
|
||||
'Bin', fields=['item_code', 'sum(actual_qty) as actual_qty'],
|
||||
filters=bin_filters, group_by='item_code'
|
||||
)
|
||||
|
||||
# convert list of dict into dict as {item_code: actual_qty}
|
||||
bin_dict = {}
|
||||
for b in bin_data:
|
||||
bin_dict[b.get('item_code')] = b.get('actual_qty')
|
||||
|
||||
for item in items_data:
|
||||
item_code = item.item_code
|
||||
item_price = item_prices.get(item_code) or {}
|
||||
item_stock_qty = bin_dict.get(item_code)
|
||||
item_stock_qty = get_stock_availability(item_code, warehouse)
|
||||
|
||||
if display_items_in_stock and not item_stock_qty:
|
||||
if not item_stock_qty:
|
||||
pass
|
||||
else:
|
||||
row = {}
|
||||
@@ -116,6 +100,13 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
||||
'items': result
|
||||
}
|
||||
|
||||
if len(res['items']) == 1:
|
||||
res['items'][0].setdefault('serial_no', serial_no)
|
||||
res['items'][0].setdefault('batch_no', batch_no)
|
||||
res['items'][0].setdefault('barcode', barcode)
|
||||
|
||||
return res
|
||||
|
||||
if serial_no:
|
||||
res.update({
|
||||
'serial_no': serial_no
|
||||
@@ -168,6 +159,7 @@ def get_item_group_condition(pos_profile):
|
||||
return cond % tuple(item_groups)
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def item_group_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
item_groups = []
|
||||
cond = "1=1"
|
||||
@@ -186,6 +178,73 @@ def item_group_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
{'txt': '%%%s%%' % txt})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_fields():
|
||||
return frappe.get_all("POS Field", fields=["label", "fieldname",
|
||||
"fieldtype", "default_value", "reqd", "read_only", "options"])
|
||||
def check_opening_entry(user):
|
||||
open_vouchers = frappe.db.get_all("POS Opening Entry",
|
||||
filters = {
|
||||
"user": user,
|
||||
"pos_closing_entry": ["in", ["", None]],
|
||||
"docstatus": 1
|
||||
},
|
||||
fields = ["name", "company", "pos_profile", "period_start_date"],
|
||||
order_by = "period_start_date desc"
|
||||
)
|
||||
|
||||
return open_vouchers
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_opening_voucher(pos_profile, company, balance_details):
|
||||
import json
|
||||
balance_details = json.loads(balance_details)
|
||||
|
||||
new_pos_opening = frappe.get_doc({
|
||||
'doctype': 'POS Opening Entry',
|
||||
"period_start_date": frappe.utils.get_datetime(),
|
||||
"posting_date": frappe.utils.getdate(),
|
||||
"user": frappe.session.user,
|
||||
"pos_profile": pos_profile,
|
||||
"company": company,
|
||||
})
|
||||
new_pos_opening.set("balance_details", balance_details)
|
||||
new_pos_opening.submit()
|
||||
|
||||
return new_pos_opening.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_past_order_list(search_term, status, limit=20):
|
||||
fields = ['name', 'grand_total', 'currency', 'customer', 'posting_time', 'posting_date']
|
||||
invoice_list = []
|
||||
|
||||
if search_term and status:
|
||||
invoices_by_customer = frappe.db.get_all('POS Invoice', filters={
|
||||
'customer': ['like', '%{}%'.format(search_term)],
|
||||
'status': status
|
||||
}, fields=fields)
|
||||
invoices_by_name = frappe.db.get_all('POS Invoice', filters={
|
||||
'name': ['like', '%{}%'.format(search_term)],
|
||||
'status': status
|
||||
}, fields=fields)
|
||||
|
||||
invoice_list = invoices_by_customer + invoices_by_name
|
||||
elif status:
|
||||
invoice_list = frappe.db.get_all('POS Invoice', filters={
|
||||
'status': status
|
||||
}, fields=fields)
|
||||
|
||||
return invoice_list
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_customer_info(fieldname, customer, value=""):
|
||||
if fieldname == 'loyalty_program':
|
||||
frappe.db.set_value('Customer', customer, 'loyalty_program', value)
|
||||
|
||||
contact = frappe.get_cached_value('Customer', customer, 'customer_primary_contact')
|
||||
|
||||
if contact:
|
||||
contact_doc = frappe.get_doc('Contact', contact)
|
||||
if fieldname == 'email_id':
|
||||
contact_doc.set('email_ids', [{ 'email_id': value, 'is_primary': 1}])
|
||||
frappe.db.set_value('Customer', customer, 'email_id', value)
|
||||
elif fieldname == 'mobile_no':
|
||||
contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}])
|
||||
frappe.db.set_value('Customer', customer, 'mobile_no', value)
|
||||
contact_doc.save()
|
||||
715
erpnext/selling/page/point_of_sale/pos_controller.js
Normal file
715
erpnext/selling/page/point_of_sale/pos_controller.js
Normal file
@@ -0,0 +1,715 @@
|
||||
{% include "erpnext/selling/page/point_of_sale/onscan.js" %}
|
||||
{% include "erpnext/selling/page/point_of_sale/pos_item_selector.js" %}
|
||||
{% include "erpnext/selling/page/point_of_sale/pos_item_cart.js" %}
|
||||
{% include "erpnext/selling/page/point_of_sale/pos_item_details.js" %}
|
||||
{% include "erpnext/selling/page/point_of_sale/pos_payment.js" %}
|
||||
{% include "erpnext/selling/page/point_of_sale/pos_number_pad.js" %}
|
||||
{% include "erpnext/selling/page/point_of_sale/pos_past_order_list.js" %}
|
||||
{% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %}
|
||||
|
||||
erpnext.PointOfSale.Controller = class {
|
||||
constructor(wrapper) {
|
||||
this.wrapper = $(wrapper).find('.layout-main-section');
|
||||
this.page = wrapper.page;
|
||||
|
||||
this.load_assets();
|
||||
}
|
||||
|
||||
load_assets() {
|
||||
// after loading assets first check if opening entry has been made
|
||||
frappe.require(['assets/erpnext/css/pos.css'], this.check_opening_entry.bind(this));
|
||||
}
|
||||
|
||||
check_opening_entry() {
|
||||
return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { "user": frappe.session.user })
|
||||
.then((r) => {
|
||||
if (r.message.length) {
|
||||
// assuming only one opening voucher is available for the current user
|
||||
this.prepare_app_defaults(r.message[0]);
|
||||
} else {
|
||||
this.create_opening_voucher();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create_opening_voucher() {
|
||||
const table_fields = [
|
||||
{ fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 },
|
||||
{ fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount",
|
||||
options: "company:company_currency", reqd: 1 }
|
||||
];
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __('Create POS Opening Entry'),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'),
|
||||
options: 'Company', fieldname: 'company', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldtype: 'Link', label: __('POS Profile'),
|
||||
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
|
||||
onchange: () => {
|
||||
const pos_profile = dialog.fields_dict.pos_profile.get_value();
|
||||
const company = dialog.fields_dict.company.get_value();
|
||||
const user = frappe.session.user
|
||||
|
||||
if (!pos_profile || !company || !user) return;
|
||||
|
||||
// auto fetch last closing entry's balance details
|
||||
frappe.db.get_list("POS Closing Entry", {
|
||||
filters: { company, pos_profile, user },
|
||||
limit: 1,
|
||||
order_by: 'period_end_date desc'
|
||||
}).then((res) => {
|
||||
if (!res.length) return;
|
||||
const pos_closing_entry = res[0];
|
||||
frappe.db.get_doc("POS Closing Entry", pos_closing_entry.name).then(({ payment_reconciliation }) => {
|
||||
dialog.fields_dict.balance_details.df.data = [];
|
||||
payment_reconciliation.forEach(pay => {
|
||||
const { mode_of_payment } = pay;
|
||||
dialog.fields_dict.balance_details.df.data.push({
|
||||
mode_of_payment: mode_of_payment
|
||||
});
|
||||
});
|
||||
dialog.fields_dict.balance_details.grid.refresh();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldname: "balance_details",
|
||||
fieldtype: "Table",
|
||||
label: "Opening Balance Details",
|
||||
cannot_add_rows: false,
|
||||
in_place_edit: true,
|
||||
reqd: 1,
|
||||
data: [],
|
||||
fields: table_fields
|
||||
}
|
||||
],
|
||||
primary_action: ({ company, pos_profile, balance_details }) => {
|
||||
if (!balance_details.length) {
|
||||
frappe.show_alert({
|
||||
message: __("Please add Mode of payments and opening balance details."),
|
||||
indicator: 'red'
|
||||
})
|
||||
frappe.utils.play_sound("error");
|
||||
return;
|
||||
}
|
||||
frappe.dom.freeze();
|
||||
return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher",
|
||||
{ pos_profile, company, balance_details })
|
||||
.then((r) => {
|
||||
frappe.dom.unfreeze();
|
||||
dialog.hide();
|
||||
if (r.message) {
|
||||
this.prepare_app_defaults(r.message);
|
||||
}
|
||||
})
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
prepare_app_defaults(data) {
|
||||
this.pos_opening = data.name;
|
||||
this.company = data.company;
|
||||
this.pos_profile = data.pos_profile;
|
||||
this.pos_opening_time = data.period_start_date;
|
||||
|
||||
frappe.db.get_value('Stock Settings', undefined, 'allow_negative_stock').then(({ message }) => {
|
||||
this.allow_negative_stock = flt(message.allow_negative_stock) || false;
|
||||
});
|
||||
|
||||
frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => {
|
||||
this.customer_groups = profile.customer_groups.map(group => group.customer_group);
|
||||
this.cart.make_customer_selector();
|
||||
});
|
||||
|
||||
this.item_stock_map = {};
|
||||
|
||||
this.make_app();
|
||||
}
|
||||
|
||||
set_opening_entry_status() {
|
||||
this.page.set_title_sub(
|
||||
`<span class="indicator orange">
|
||||
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}">
|
||||
Opened at ${moment(this.pos_opening_time).format("Do MMMM, h:mma")}
|
||||
</a>
|
||||
</span>`);
|
||||
}
|
||||
|
||||
make_app() {
|
||||
return frappe.run_serially([
|
||||
() => frappe.dom.freeze(),
|
||||
() => {
|
||||
this.set_opening_entry_status();
|
||||
this.prepare_dom();
|
||||
this.prepare_components();
|
||||
this.prepare_menu();
|
||||
},
|
||||
() => this.make_new_invoice(),
|
||||
() => frappe.dom.unfreeze(),
|
||||
() => this.page.set_title(__('Point of Sale')),
|
||||
]);
|
||||
}
|
||||
|
||||
prepare_dom() {
|
||||
this.wrapper.append(`
|
||||
<div class="app grid grid-cols-10 pt-8 gap-6"></div>`
|
||||
);
|
||||
|
||||
this.$components_wrapper = this.wrapper.find('.app');
|
||||
}
|
||||
|
||||
prepare_components() {
|
||||
this.init_item_selector();
|
||||
this.init_item_details();
|
||||
this.init_item_cart();
|
||||
this.init_payments();
|
||||
this.init_recent_order_list();
|
||||
this.init_order_summary();
|
||||
}
|
||||
|
||||
prepare_menu() {
|
||||
var me = this;
|
||||
this.page.clear_menu();
|
||||
|
||||
this.page.add_menu_item(__("Form View"), function () {
|
||||
frappe.model.sync(me.frm.doc);
|
||||
frappe.set_route("Form", me.frm.doc.doctype, me.frm.doc.name);
|
||||
});
|
||||
|
||||
this.page.add_menu_item(__("Toggle Recent Orders"), () => {
|
||||
const show = this.recent_order_list.$component.hasClass('d-none');
|
||||
this.toggle_recent_order_list(show);
|
||||
});
|
||||
|
||||
this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this));
|
||||
|
||||
frappe.ui.keys.on("ctrl+s", this.save_draft_invoice.bind(this));
|
||||
|
||||
this.page.add_menu_item(__('Close the POS'), this.close_pos.bind(this));
|
||||
|
||||
frappe.ui.keys.on("shift+ctrl+s", this.close_pos.bind(this));
|
||||
}
|
||||
|
||||
save_draft_invoice() {
|
||||
if (!this.$components_wrapper.is(":visible")) return;
|
||||
|
||||
if (this.frm.doc.items.length == 0) {
|
||||
frappe.show_alert({
|
||||
message:__("You must add atleast one item to save it as draft."),
|
||||
indicator:'red'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.frm.save(undefined, undefined, undefined, () => {
|
||||
frappe.show_alert({
|
||||
message:__("There was an error saving the document."),
|
||||
indicator:'red'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
}).then(() => {
|
||||
frappe.run_serially([
|
||||
() => frappe.dom.freeze(),
|
||||
() => this.make_new_invoice(),
|
||||
() => frappe.dom.unfreeze(),
|
||||
]);
|
||||
})
|
||||
}
|
||||
|
||||
close_pos() {
|
||||
if (!this.$components_wrapper.is(":visible")) return;
|
||||
|
||||
let voucher = frappe.model.get_new_doc('POS Closing Entry');
|
||||
voucher.pos_profile = this.frm.doc.pos_profile;
|
||||
voucher.user = frappe.session.user;
|
||||
voucher.company = this.frm.doc.company;
|
||||
voucher.pos_opening_entry = this.pos_opening;
|
||||
voucher.period_end_date = frappe.datetime.now_datetime();
|
||||
voucher.posting_date = frappe.datetime.now_date();
|
||||
frappe.set_route('Form', 'POS Closing Entry', voucher.name);
|
||||
}
|
||||
|
||||
init_item_selector() {
|
||||
this.item_selector = new erpnext.PointOfSale.ItemSelector({
|
||||
wrapper: this.$components_wrapper,
|
||||
pos_profile: this.pos_profile,
|
||||
events: {
|
||||
item_selected: args => this.on_cart_update(args),
|
||||
|
||||
get_frm: () => this.frm || {},
|
||||
|
||||
get_allowed_item_group: () => this.item_groups
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
init_item_cart() {
|
||||
this.cart = new erpnext.PointOfSale.ItemCart({
|
||||
wrapper: this.$components_wrapper,
|
||||
events: {
|
||||
get_frm: () => this.frm,
|
||||
|
||||
cart_item_clicked: (item_code, batch_no, uom) => {
|
||||
const item_row = this.frm.doc.items.find(
|
||||
i => i.item_code === item_code
|
||||
&& i.uom === uom
|
||||
&& (!batch_no || (batch_no && i.batch_no === batch_no))
|
||||
);
|
||||
this.item_details.toggle_item_details_section(item_row);
|
||||
},
|
||||
|
||||
numpad_event: (value, action) => this.update_item_field(value, action),
|
||||
|
||||
checkout: () => this.payment.checkout(),
|
||||
|
||||
edit_cart: () => this.payment.edit_cart(),
|
||||
|
||||
customer_details_updated: (details) => {
|
||||
this.customer_details = details;
|
||||
// will add/remove LP payment method
|
||||
this.payment.render_loyalty_points_payment_mode();
|
||||
},
|
||||
|
||||
get_allowed_customer_group: () => this.customer_groups
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
init_item_details() {
|
||||
this.item_details = new erpnext.PointOfSale.ItemDetails({
|
||||
wrapper: this.$components_wrapper,
|
||||
events: {
|
||||
get_frm: () => this.frm,
|
||||
|
||||
toggle_item_selector: (minimize) => {
|
||||
this.item_selector.resize_selector(minimize);
|
||||
this.cart.toggle_numpad(minimize);
|
||||
},
|
||||
|
||||
form_updated: async (cdt, cdn, fieldname, value) => {
|
||||
const item_row = frappe.model.get_doc(cdt, cdn);
|
||||
if (item_row && item_row[fieldname] != value) {
|
||||
|
||||
if (fieldname === 'qty' && flt(value) == 0) {
|
||||
this.remove_item_from_cart();
|
||||
return;
|
||||
}
|
||||
|
||||
const { item_code, batch_no, uom } = this.item_details.current_item;
|
||||
const event = {
|
||||
field: fieldname,
|
||||
value,
|
||||
item: { item_code, batch_no, uom }
|
||||
}
|
||||
return this.on_cart_update(event)
|
||||
}
|
||||
},
|
||||
|
||||
item_field_focused: (fieldname) => {
|
||||
this.cart.toggle_numpad_field_edit(fieldname);
|
||||
},
|
||||
set_value_in_current_cart_item: (selector, value) => {
|
||||
this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item);
|
||||
},
|
||||
clone_new_batch_item_in_frm: (batch_serial_map, current_item) => {
|
||||
// called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches
|
||||
// for each unique batch new item row is added in the form & cart
|
||||
Object.keys(batch_serial_map).forEach(batch => {
|
||||
const { item_code, batch_no } = current_item;
|
||||
const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no);
|
||||
const new_row = this.frm.add_child("items", { ...item_to_clone });
|
||||
// update new serialno and batch
|
||||
new_row.batch_no = batch;
|
||||
new_row.serial_no = batch_serial_map[batch].join(`\n`);
|
||||
new_row.qty = batch_serial_map[batch].length;
|
||||
this.frm.doc.items.forEach(row => {
|
||||
if (item_code === row.item_code) {
|
||||
this.update_cart_html(row);
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
remove_item_from_cart: () => this.remove_item_from_cart(),
|
||||
get_item_stock_map: () => this.item_stock_map,
|
||||
close_item_details: () => {
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
this.cart.prev_action = undefined;
|
||||
this.cart.toggle_item_highlight();
|
||||
},
|
||||
get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init_payments() {
|
||||
this.payment = new erpnext.PointOfSale.Payment({
|
||||
wrapper: this.$components_wrapper,
|
||||
events: {
|
||||
get_frm: () => this.frm || {},
|
||||
|
||||
get_customer_details: () => this.customer_details || {},
|
||||
|
||||
toggle_other_sections: (show) => {
|
||||
if (show) {
|
||||
this.item_details.$component.hasClass('d-none') ? '' : this.item_details.$component.addClass('d-none');
|
||||
this.item_selector.$component.addClass('d-none');
|
||||
} else {
|
||||
this.item_selector.$component.removeClass('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
submit_invoice: () => {
|
||||
this.frm.savesubmit()
|
||||
.then((r) => {
|
||||
// this.set_invoice_status();
|
||||
this.toggle_components(false);
|
||||
this.order_summary.toggle_component(true);
|
||||
this.order_summary.load_summary_of(this.frm.doc, true);
|
||||
frappe.show_alert({
|
||||
indicator: 'green',
|
||||
message: __(`POS invoice ${r.doc.name} created succesfully`)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init_recent_order_list() {
|
||||
this.recent_order_list = new erpnext.PointOfSale.PastOrderList({
|
||||
wrapper: this.$components_wrapper,
|
||||
events: {
|
||||
open_invoice_data: (name) => {
|
||||
frappe.db.get_doc('POS Invoice', name).then((doc) => {
|
||||
this.order_summary.load_summary_of(doc);
|
||||
});
|
||||
},
|
||||
reset_summary: () => this.order_summary.show_summary_placeholder()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
init_order_summary() {
|
||||
this.order_summary = new erpnext.PointOfSale.PastOrderSummary({
|
||||
wrapper: this.$components_wrapper,
|
||||
events: {
|
||||
get_frm: () => this.frm,
|
||||
|
||||
process_return: (name) => {
|
||||
this.recent_order_list.toggle_component(false);
|
||||
frappe.db.get_doc('POS Invoice', name).then((doc) => {
|
||||
frappe.run_serially([
|
||||
() => this.make_return_invoice(doc),
|
||||
() => this.cart.load_invoice(),
|
||||
() => this.item_selector.toggle_component(true)
|
||||
]);
|
||||
});
|
||||
},
|
||||
edit_order: (name) => {
|
||||
this.recent_order_list.toggle_component(false);
|
||||
frappe.run_serially([
|
||||
() => this.frm.refresh(name),
|
||||
() => this.cart.load_invoice(),
|
||||
() => this.item_selector.toggle_component(true)
|
||||
]);
|
||||
},
|
||||
new_order: () => {
|
||||
frappe.run_serially([
|
||||
() => frappe.dom.freeze(),
|
||||
() => this.make_new_invoice(),
|
||||
() => this.item_selector.toggle_component(true),
|
||||
() => frappe.dom.unfreeze(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
toggle_recent_order_list(show) {
|
||||
this.toggle_components(!show);
|
||||
this.recent_order_list.toggle_component(show);
|
||||
this.order_summary.toggle_component(show);
|
||||
}
|
||||
|
||||
toggle_components(show) {
|
||||
this.cart.toggle_component(show);
|
||||
this.item_selector.toggle_component(show);
|
||||
|
||||
// do not show item details or payment if recent order is toggled off
|
||||
!show ? (this.item_details.toggle_component(false) || this.payment.toggle_component(false)) : '';
|
||||
}
|
||||
|
||||
make_new_invoice() {
|
||||
return frappe.run_serially([
|
||||
() => this.make_sales_invoice_frm(),
|
||||
() => this.set_pos_profile_data(),
|
||||
() => this.set_pos_profile_status(),
|
||||
() => this.cart.load_invoice(),
|
||||
]);
|
||||
}
|
||||
|
||||
make_sales_invoice_frm() {
|
||||
const doctype = 'POS Invoice';
|
||||
return new Promise(resolve => {
|
||||
if (this.frm) {
|
||||
this.frm = this.get_new_frm(this.frm);
|
||||
this.frm.doc.items = [];
|
||||
this.frm.doc.is_pos = 1
|
||||
resolve();
|
||||
} else {
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
this.frm = this.get_new_frm();
|
||||
this.frm.doc.items = [];
|
||||
this.frm.doc.is_pos = 1
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_new_frm(_frm) {
|
||||
const doctype = 'POS Invoice';
|
||||
const page = $('<div>');
|
||||
const frm = _frm || new frappe.ui.form.Form(doctype, page, false);
|
||||
const name = frappe.model.make_new_doc_and_get_name(doctype, true);
|
||||
frm.refresh(name);
|
||||
|
||||
return frm;
|
||||
}
|
||||
|
||||
async make_return_invoice(doc) {
|
||||
frappe.dom.freeze();
|
||||
this.frm = this.get_new_frm(this.frm);
|
||||
this.frm.doc.items = [];
|
||||
const res = await frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
|
||||
args: {
|
||||
'source_name': doc.name,
|
||||
'target_doc': this.frm.doc
|
||||
}
|
||||
});
|
||||
frappe.model.sync(res.message);
|
||||
await this.set_pos_profile_data();
|
||||
frappe.dom.unfreeze();
|
||||
}
|
||||
|
||||
set_pos_profile_data() {
|
||||
if (this.company && !this.frm.doc.company) this.frm.doc.company = this.company;
|
||||
if (this.pos_profile && !this.frm.doc.pos_profile) this.frm.doc.pos_profile = this.pos_profile;
|
||||
if (!this.frm.doc.company) return;
|
||||
|
||||
return new Promise(resolve => {
|
||||
return this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: "set_missing_values",
|
||||
}).then((r) => {
|
||||
if(!r.exc) {
|
||||
if (!this.frm.doc.pos_profile) {
|
||||
frappe.dom.unfreeze();
|
||||
this.raise_exception_for_pos_profile();
|
||||
}
|
||||
this.frm.trigger("update_stock");
|
||||
this.frm.trigger('calculate_taxes_and_totals');
|
||||
if(this.frm.doc.taxes_and_charges) this.frm.script_manager.trigger("taxes_and_charges");
|
||||
frappe.model.set_default_values(this.frm.doc);
|
||||
if (r.message) {
|
||||
this.frm.pos_print_format = r.message.print_format || "";
|
||||
this.frm.meta.default_print_format = r.message.print_format || "";
|
||||
this.frm.allow_edit_rate = r.message.allow_edit_rate;
|
||||
this.frm.allow_edit_discount = r.message.allow_edit_discount;
|
||||
this.frm.doc.campaign = r.message.campaign;
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
raise_exception_for_pos_profile() {
|
||||
setTimeout(() => frappe.set_route('List', 'POS Profile'), 2000);
|
||||
frappe.throw(__("POS Profile is required to use Point-of-Sale"));
|
||||
}
|
||||
|
||||
set_invoice_status() {
|
||||
const [status, indicator] = frappe.listview_settings["POS Invoice"].get_indicator(this.frm.doc);
|
||||
this.page.set_indicator(__(`${status}`), indicator);
|
||||
}
|
||||
|
||||
set_pos_profile_status() {
|
||||
this.page.set_indicator(__(`${this.pos_profile}`), "blue");
|
||||
}
|
||||
|
||||
async on_cart_update(args) {
|
||||
frappe.dom.freeze();
|
||||
try {
|
||||
let { field, value, item } = args;
|
||||
const { item_code, batch_no, serial_no, uom } = item;
|
||||
let item_row = this.get_item_from_frm(item_code, batch_no, uom);
|
||||
|
||||
const item_selected_from_selector = field === 'qty' && value === "+1"
|
||||
|
||||
if (item_row) {
|
||||
item_selected_from_selector && (value = item_row.qty + flt(value))
|
||||
|
||||
field === 'qty' && (value = flt(value));
|
||||
|
||||
if (field === 'qty' && value > 0 && !this.allow_negative_stock)
|
||||
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
|
||||
|
||||
if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) {
|
||||
await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
|
||||
this.update_cart_html(item_row);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (!this.frm.doc.customer) {
|
||||
frappe.dom.unfreeze();
|
||||
frappe.show_alert({
|
||||
message: __('You must select a customer before adding an item.'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
return;
|
||||
}
|
||||
item_selected_from_selector && (value = flt(value))
|
||||
|
||||
const args = { item_code, batch_no, [field]: value };
|
||||
|
||||
if (serial_no) args['serial_no'] = serial_no;
|
||||
|
||||
if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0;
|
||||
|
||||
item_row = this.frm.add_child('items', args);
|
||||
|
||||
if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
|
||||
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
|
||||
|
||||
await this.trigger_new_item_events(item_row);
|
||||
|
||||
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
|
||||
this.update_cart_html(item_row);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
frappe.dom.unfreeze();
|
||||
}
|
||||
}
|
||||
|
||||
get_item_from_frm(item_code, batch_no, uom) {
|
||||
const has_batch_no = batch_no;
|
||||
return this.frm.doc.items.find(
|
||||
i => i.item_code === item_code
|
||||
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
|
||||
&& (i.uom === uom)
|
||||
);
|
||||
}
|
||||
|
||||
edit_item_details_of(item_row) {
|
||||
this.item_details.toggle_item_details_section(item_row);
|
||||
}
|
||||
|
||||
is_current_item_being_edited(item_row) {
|
||||
const { item_code, batch_no } = this.item_details.current_item;
|
||||
|
||||
return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true;
|
||||
}
|
||||
|
||||
update_cart_html(item_row, remove_item) {
|
||||
this.cart.update_item_html(item_row, remove_item);
|
||||
this.cart.update_totals_section(this.frm);
|
||||
}
|
||||
|
||||
check_serial_batch_selection_needed(item_row) {
|
||||
// right now item details is shown for every type of item.
|
||||
// if item details is not shown for every item then this fn will be needed
|
||||
const serialized = item_row.has_serial_no;
|
||||
const batched = item_row.has_batch_no;
|
||||
const no_serial_selected = !item_row.serial_no;
|
||||
const no_batch_selected = !item_row.batch_no;
|
||||
|
||||
if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
|
||||
(serialized && batched && (no_batch_selected || no_serial_selected))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async trigger_new_item_events(item_row) {
|
||||
await this.frm.script_manager.trigger('item_code', item_row.doctype, item_row.name)
|
||||
await this.frm.script_manager.trigger('qty', item_row.doctype, item_row.name)
|
||||
}
|
||||
|
||||
async check_stock_availability(item_row, qty_needed, warehouse) {
|
||||
const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message;
|
||||
|
||||
frappe.dom.unfreeze();
|
||||
if (!(available_qty > 0)) {
|
||||
frappe.model.clear_doc(item_row.doctype, item_row.name);
|
||||
frappe.throw(__(`Item Code: ${item_row.item_code.bold()} is not available under warehouse ${warehouse.bold()}.`))
|
||||
} else if (available_qty < qty_needed) {
|
||||
frappe.show_alert({
|
||||
message: __(`Stock quantity not enough for Item Code: ${item_row.item_code.bold()} under warehouse ${warehouse.bold()}.
|
||||
Available quantity ${available_qty.toString().bold()}.`),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
this.item_details.qty_control.set_value(flt(available_qty));
|
||||
}
|
||||
frappe.dom.freeze();
|
||||
}
|
||||
|
||||
get_available_stock(item_code, warehouse) {
|
||||
const me = this;
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability",
|
||||
args: {
|
||||
'item_code': item_code,
|
||||
'warehouse': warehouse,
|
||||
},
|
||||
callback(res) {
|
||||
if (!me.item_stock_map[item_code])
|
||||
me.item_stock_map[item_code] = {}
|
||||
me.item_stock_map[item_code][warehouse] = res.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update_item_field(value, field_or_action) {
|
||||
if (field_or_action === 'checkout') {
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
} else if (field_or_action === 'remove') {
|
||||
this.remove_item_from_cart();
|
||||
} else {
|
||||
const field_control = this.item_details[`${field_or_action}_control`];
|
||||
if (!field_control) return;
|
||||
field_control.set_focus();
|
||||
value != "" && field_control.set_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
remove_item_from_cart() {
|
||||
frappe.dom.freeze();
|
||||
const { doctype, name, current_item } = this.item_details;
|
||||
|
||||
frappe.model.set_value(doctype, name, 'qty', 0);
|
||||
|
||||
this.frm.script_manager.trigger('qty', doctype, name).then(() => {
|
||||
frappe.model.clear_doc(doctype, name);
|
||||
this.update_cart_html(current_item, true);
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
frappe.dom.unfreeze();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
951
erpnext/selling/page/point_of_sale/pos_item_cart.js
Normal file
951
erpnext/selling/page/point_of_sale/pos_item_cart.js
Normal file
@@ -0,0 +1,951 @@
|
||||
erpnext.PointOfSale.ItemCart = class {
|
||||
constructor({ wrapper, events }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
this.customer_info = undefined;
|
||||
|
||||
this.init_component();
|
||||
}
|
||||
|
||||
init_component() {
|
||||
this.prepare_dom();
|
||||
this.init_child_components();
|
||||
this.bind_events();
|
||||
this.attach_shortcuts();
|
||||
}
|
||||
|
||||
prepare_dom() {
|
||||
this.wrapper.append(
|
||||
`<section class="col-span-4 flex flex-col shadow rounded item-cart bg-white mx-h-70 h-100"></section>`
|
||||
)
|
||||
|
||||
this.$component = this.wrapper.find('.item-cart');
|
||||
}
|
||||
|
||||
init_child_components() {
|
||||
this.init_customer_selector();
|
||||
this.init_cart_components();
|
||||
}
|
||||
|
||||
init_customer_selector() {
|
||||
this.$component.append(
|
||||
`<div class="customer-section rounded flex flex-col m-8 mb-0"></div>`
|
||||
)
|
||||
this.$customer_section = this.$component.find('.customer-section');
|
||||
}
|
||||
|
||||
reset_customer_selector() {
|
||||
const frm = this.events.get_frm();
|
||||
frm.set_value('customer', '');
|
||||
this.$customer_section.removeClass('border pr-4 pl-4');
|
||||
this.make_customer_selector();
|
||||
this.customer_field.set_focus();
|
||||
}
|
||||
|
||||
init_cart_components() {
|
||||
this.$component.append(
|
||||
`<div class="cart-container flex flex-col items-center rounded flex-1 relative">
|
||||
<div class="absolute flex flex-col p-8 pt-0 w-full h-full">
|
||||
<div class="flex text-grey cart-header pt-2 pb-2 p-4 mt-2 mb-2 w-full f-shrink-0">
|
||||
<div class="flex-1">Item</div>
|
||||
<div class="mr-4">Qty</div>
|
||||
<div class="rate-list-header mr-1 text-right">Amount</div>
|
||||
</div>
|
||||
<div class="cart-items-section flex flex-col flex-1 scroll-y rounded w-full"></div>
|
||||
<div class="cart-totals-section flex flex-col w-full mt-4 f-shrink-0"></div>
|
||||
<div class="numpad-section flex flex-col mt-4 d-none w-full p-8 pt-0 pb-0 f-shrink-0"></div>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
this.$cart_container = this.$component.find('.cart-container');
|
||||
|
||||
this.make_cart_totals_section();
|
||||
this.make_cart_items_section();
|
||||
this.make_cart_numpad();
|
||||
}
|
||||
|
||||
make_cart_items_section() {
|
||||
this.$cart_header = this.$component.find('.cart-header');
|
||||
this.$cart_items_wrapper = this.$component.find('.cart-items-section');
|
||||
|
||||
this.make_no_items_placeholder();
|
||||
}
|
||||
|
||||
make_no_items_placeholder() {
|
||||
this.$cart_header.addClass('d-none');
|
||||
this.$cart_items_wrapper.html(
|
||||
`<div class="no-item-wrapper flex items-center h-18">
|
||||
<div class="flex-1 text-center text-grey">No items in cart</div>
|
||||
</div>`
|
||||
)
|
||||
this.$cart_items_wrapper.addClass('mt-4 border-grey border-dashed');
|
||||
}
|
||||
|
||||
make_cart_totals_section() {
|
||||
this.$totals_section = this.$component.find('.cart-totals-section');
|
||||
|
||||
this.$totals_section.append(
|
||||
`<div class="add-discount flex items-center pt-4 pb-4 pr-4 pl-4 text-grey pointer no-select d-none">
|
||||
+ Add Discount
|
||||
</div>
|
||||
<div class="border border-grey rounded">
|
||||
<div class="net-total flex justify-between items-center h-16 pr-8 pl-8 border-b-grey">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-md text-dark-grey text-bold">Net Total</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<div class="text-md text-dark-grey text-bold">0.00</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="taxes"></div>
|
||||
<div class="grand-total flex justify-between items-center h-16 pr-8 pl-8 border-b-grey">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-md text-dark-grey text-bold">Grand Total</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<div class="text-md text-dark-grey text-bold">0.00</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkout-btn flex items-center justify-center h-16 pr-8 pl-8 text-center text-grey no-select pointer rounded-b text-md text-bold">
|
||||
Checkout
|
||||
</div>
|
||||
<div class="edit-cart-btn flex items-center justify-center h-16 pr-8 pl-8 text-center text-grey no-select pointer d-none text-md text-bold">
|
||||
Edit Cart
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this.$add_discount_elem = this.$component.find(".add-discount");
|
||||
}
|
||||
|
||||
make_cart_numpad() {
|
||||
this.$numpad_section = this.$component.find('.numpad-section');
|
||||
|
||||
this.number_pad = new erpnext.PointOfSale.NumberPad({
|
||||
wrapper: this.$numpad_section,
|
||||
events: {
|
||||
numpad_event: this.on_numpad_event.bind(this)
|
||||
},
|
||||
cols: 5,
|
||||
keys: [
|
||||
[ 1, 2, 3, 'Quantity' ],
|
||||
[ 4, 5, 6, 'Discount' ],
|
||||
[ 7, 8, 9, 'Rate' ],
|
||||
[ '.', 0, 'Delete', 'Remove' ]
|
||||
],
|
||||
css_classes: [
|
||||
[ '', '', '', 'col-span-2' ],
|
||||
[ '', '', '', 'col-span-2' ],
|
||||
[ '', '', '', 'col-span-2' ],
|
||||
[ '', '', '', 'col-span-2 text-bold text-danger' ]
|
||||
],
|
||||
fieldnames_map: { 'Quantity': 'qty', 'Discount': 'discount_percentage' }
|
||||
})
|
||||
|
||||
this.$numpad_section.prepend(
|
||||
`<div class="flex mb-2 justify-between">
|
||||
<span class="numpad-net-total"></span>
|
||||
<span class="numpad-grand-total"></span>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this.$numpad_section.append(
|
||||
`<div class="numpad-btn checkout-btn flex items-center justify-center h-16 pr-8 pl-8 bg-primary
|
||||
text-center text-white no-select pointer rounded text-md text-bold mt-4" data-button-value="checkout">
|
||||
Checkout
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
const me = this;
|
||||
this.$customer_section.on('click', '.add-remove-customer', function (e) {
|
||||
const customer_info_is_visible = me.$cart_container.hasClass('d-none');
|
||||
customer_info_is_visible ?
|
||||
me.toggle_customer_info(false) : me.reset_customer_selector();
|
||||
});
|
||||
|
||||
this.$customer_section.on('click', '.customer-header', function(e) {
|
||||
// don't triggger the event if .add-remove-customer btn is clicked which is under .customer-header
|
||||
if ($(e.target).closest('.add-remove-customer').length) return;
|
||||
|
||||
const show = !me.$cart_container.hasClass('d-none');
|
||||
me.toggle_customer_info(show);
|
||||
});
|
||||
|
||||
this.$cart_items_wrapper.on('click', '.cart-item-wrapper', function() {
|
||||
const $cart_item = $(this);
|
||||
|
||||
me.toggle_item_highlight(this);
|
||||
|
||||
const payment_section_hidden = me.$totals_section.find('.edit-cart-btn').hasClass('d-none');
|
||||
if (!payment_section_hidden) {
|
||||
// payment section is visible
|
||||
// edit cart first and then open item details section
|
||||
me.$totals_section.find(".edit-cart-btn").click();
|
||||
}
|
||||
|
||||
const item_code = unescape($cart_item.attr('data-item-code'));
|
||||
const batch_no = unescape($cart_item.attr('data-batch-no'));
|
||||
const uom = unescape($cart_item.attr('data-uom'));
|
||||
me.events.cart_item_clicked(item_code, batch_no, uom);
|
||||
this.numpad_value = '';
|
||||
});
|
||||
|
||||
this.$component.on('click', '.checkout-btn', function() {
|
||||
if (!$(this).hasClass('bg-primary')) return;
|
||||
|
||||
me.events.checkout();
|
||||
me.toggle_checkout_btn(false);
|
||||
|
||||
me.$add_discount_elem.removeClass("d-none");
|
||||
});
|
||||
|
||||
this.$totals_section.on('click', '.edit-cart-btn', () => {
|
||||
this.events.edit_cart();
|
||||
this.toggle_checkout_btn(true);
|
||||
|
||||
this.$add_discount_elem.addClass("d-none");
|
||||
});
|
||||
|
||||
this.$component.on('click', '.add-discount', () => {
|
||||
const can_edit_discount = this.$add_discount_elem.find('.edit-discount').length;
|
||||
|
||||
if(!this.discount_field || can_edit_discount) this.show_discount_control();
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Invoice", "paid_amount", frm => {
|
||||
// called when discount is applied
|
||||
this.update_totals_section(frm);
|
||||
});
|
||||
}
|
||||
|
||||
attach_shortcuts() {
|
||||
for (let row of this.number_pad.keys) {
|
||||
for (let btn of row) {
|
||||
let shortcut_key = `ctrl+${frappe.scrub(String(btn))[0]}`;
|
||||
if (btn === 'Delete') shortcut_key = 'ctrl+backspace';
|
||||
if (btn === 'Remove') shortcut_key = 'shift+ctrl+backspace'
|
||||
if (btn === '.') shortcut_key = 'ctrl+>';
|
||||
|
||||
// to account for fieldname map
|
||||
const fieldname = this.number_pad.fieldnames[btn] ? this.number_pad.fieldnames[btn] :
|
||||
typeof btn === 'string' ? frappe.scrub(btn) : btn;
|
||||
|
||||
frappe.ui.keys.on(`${shortcut_key}`, () => {
|
||||
const cart_is_visible = this.$component.is(":visible");
|
||||
if (cart_is_visible && this.item_is_selected && this.$numpad_section.is(":visible")) {
|
||||
this.$numpad_section.find(`.numpad-btn[data-button-value="${fieldname}"]`).click();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
frappe.ui.keys.on("ctrl+enter", () => {
|
||||
const cart_is_visible = this.$component.is(":visible");
|
||||
const payment_section_hidden = this.$totals_section.find('.edit-cart-btn').hasClass('d-none');
|
||||
if (cart_is_visible && payment_section_hidden) {
|
||||
this.$component.find(".checkout-btn").click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggle_item_highlight(item) {
|
||||
const $cart_item = $(item);
|
||||
const item_is_highlighted = $cart_item.hasClass("shadow");
|
||||
|
||||
if (!item || item_is_highlighted) {
|
||||
this.item_is_selected = false;
|
||||
this.$cart_container.find('.cart-item-wrapper').removeClass("shadow").css("opacity", "1");
|
||||
} else {
|
||||
$cart_item.addClass("shadow");
|
||||
this.item_is_selected = true;
|
||||
this.$cart_container.find('.cart-item-wrapper').css("opacity", "1");
|
||||
this.$cart_container.find('.cart-item-wrapper').not(item).removeClass("shadow").css("opacity", "0.65");
|
||||
}
|
||||
// highlight with inner shadow
|
||||
// $cart_item.addClass("shadow-inner bg-selected");
|
||||
// me.$cart_container.find('.cart-item-wrapper').not(this).removeClass("shadow-inner bg-selected");
|
||||
}
|
||||
|
||||
make_customer_selector() {
|
||||
this.$customer_section.html(`<div class="customer-search-field flex flex-1 items-center"></div>`);
|
||||
const me = this;
|
||||
const query = { query: 'erpnext.controllers.queries.customer_query' };
|
||||
const allowed_customer_group = this.events.get_allowed_customer_group() || [];
|
||||
if (allowed_customer_group.length) {
|
||||
query.filters = {
|
||||
customer_group: ['in', allowed_customer_group]
|
||||
}
|
||||
}
|
||||
this.customer_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Customer'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Customer',
|
||||
placeholder: __('Search by customer name, phone, email.'),
|
||||
get_query: () => query,
|
||||
onchange: function() {
|
||||
if (this.value) {
|
||||
const frm = me.events.get_frm();
|
||||
frappe.dom.freeze();
|
||||
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'customer', this.value);
|
||||
frm.script_manager.trigger('customer', frm.doc.doctype, frm.doc.name).then(() => {
|
||||
frappe.run_serially([
|
||||
() => me.fetch_customer_details(this.value),
|
||||
() => me.events.customer_details_updated(me.customer_info),
|
||||
() => me.update_customer_section(),
|
||||
() => me.update_totals_section(),
|
||||
() => frappe.dom.unfreeze()
|
||||
]);
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
parent: this.$customer_section.find('.customer-search-field'),
|
||||
render_input: true,
|
||||
});
|
||||
this.customer_field.toggle_label(false);
|
||||
}
|
||||
|
||||
fetch_customer_details(customer) {
|
||||
if (customer) {
|
||||
return new Promise((resolve) => {
|
||||
frappe.db.get_value('Customer', customer, ["email_id", "mobile_no", "image", "loyalty_program"]).then(({ message }) => {
|
||||
const { loyalty_program } = message;
|
||||
// if loyalty program then fetch loyalty points too
|
||||
if (loyalty_program) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details_with_points",
|
||||
args: { customer, loyalty_program, "silent": true },
|
||||
callback: (r) => {
|
||||
const { loyalty_points, conversion_factor } = r.message;
|
||||
if (!r.exc) {
|
||||
this.customer_info = { ...message, customer, loyalty_points, conversion_factor };
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.customer_info = { ...message, customer };
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
this.customer_info = {}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
show_discount_control() {
|
||||
this.$add_discount_elem.removeClass("pr-4 pl-4");
|
||||
this.$add_discount_elem.html(
|
||||
`<div class="add-dicount-field flex flex-1 items-center"></div>
|
||||
<div class="submit-field flex items-center"></div>`
|
||||
);
|
||||
const me = this;
|
||||
|
||||
this.discount_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Discount'),
|
||||
fieldtype: 'Data',
|
||||
placeholder: __('Enter discount percentage.'),
|
||||
onchange: function() {
|
||||
if (this.value || this.value == 0) {
|
||||
const frm = me.events.get_frm();
|
||||
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value));
|
||||
me.hide_discount_control(this.value);
|
||||
}
|
||||
},
|
||||
},
|
||||
parent: this.$add_discount_elem.find('.add-dicount-field'),
|
||||
render_input: true,
|
||||
});
|
||||
this.discount_field.toggle_label(false);
|
||||
this.discount_field.set_focus();
|
||||
}
|
||||
|
||||
hide_discount_control(discount) {
|
||||
this.$add_discount_elem.addClass('pr-4 pl-4');
|
||||
this.$add_discount_elem.html(
|
||||
`<svg class="mr-2" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
||||
</svg>
|
||||
<div class="edit-discount p-1 pr-3 pl-3 text-dark-grey rounded w-fit bg-green-200 mb-2">
|
||||
${String(discount).bold()}% off
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
update_customer_section() {
|
||||
const { customer, email_id='', mobile_no='', image } = this.customer_info || {};
|
||||
|
||||
if (customer) {
|
||||
this.$customer_section.addClass('border pr-4 pl-4').html(
|
||||
`<div class="customer-details flex flex-col">
|
||||
<div class="customer-header flex items-center rounded h-18 pointer">
|
||||
${get_customer_image()}
|
||||
<div class="customer-name flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap">
|
||||
<div class="text-md text-dark-grey text-bold">${customer}</div>
|
||||
${get_customer_description()}
|
||||
</div>
|
||||
<div class="f-shrink-0 add-remove-customer flex items-center pointer" data-customer="${escape(customer)}">
|
||||
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
} else {
|
||||
// reset customer selector
|
||||
this.reset_customer_selector();
|
||||
}
|
||||
|
||||
function get_customer_description() {
|
||||
if (!email_id && !mobile_no) {
|
||||
return `<div class="text-grey-200 italic">Click to add email / phone</div>`
|
||||
} else if (email_id && !mobile_no) {
|
||||
return `<div class="text-grey">${email_id}</div>`
|
||||
} else if (mobile_no && !email_id) {
|
||||
return `<div class="text-grey">${mobile_no}</div>`
|
||||
} else {
|
||||
return `<div class="text-grey">${email_id} | ${mobile_no}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
function get_customer_image() {
|
||||
if (image) {
|
||||
return `<div class="icon flex items-center justify-center w-12 h-12 rounded bg-light-grey mr-4 text-grey-200">
|
||||
<img class="h-full" src="${image}" alt="${image}" style="object-fit: cover;">
|
||||
</div>`
|
||||
} else {
|
||||
return `<div class="icon flex items-center justify-center w-12 h-12 rounded bg-light-grey mr-4 text-grey-200 text-md">
|
||||
${frappe.get_abbr(customer)}
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_totals_section(frm) {
|
||||
if (!frm) frm = this.events.get_frm();
|
||||
|
||||
this.render_net_total(frm.doc.base_net_total);
|
||||
this.render_grand_total(frm.doc.base_grand_total);
|
||||
|
||||
const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }})
|
||||
this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes);
|
||||
}
|
||||
|
||||
render_net_total(value) {
|
||||
const currency = this.events.get_frm().doc.currency;
|
||||
this.$totals_section.find('.net-total').html(
|
||||
`<div class="flex flex-col">
|
||||
<div class="text-md text-dark-grey text-bold">Net Total</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this.$numpad_section.find('.numpad-net-total').html(`Net Total: <span class="text-bold">${format_currency(value, currency)}</span>`)
|
||||
}
|
||||
|
||||
render_grand_total(value) {
|
||||
const currency = this.events.get_frm().doc.currency;
|
||||
this.$totals_section.find('.grand-total').html(
|
||||
`<div class="flex flex-col">
|
||||
<div class="text-md text-dark-grey text-bold">Grand Total</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this.$numpad_section.find('.numpad-grand-total').html(`Grand Total: <span class="text-bold">${format_currency(value, currency)}</span>`)
|
||||
}
|
||||
|
||||
render_taxes(value, taxes) {
|
||||
if (taxes.length) {
|
||||
const currency = this.events.get_frm().doc.currency;
|
||||
this.$totals_section.find('.taxes').html(
|
||||
`<div class="flex items-center justify-between h-16 pr-8 pl-8 border-b-grey">
|
||||
<div class="flex">
|
||||
<div class="text-md text-dark-grey text-bold w-fit">Tax Charges</div>
|
||||
<div class="flex ml-6 text-dark-grey">
|
||||
${
|
||||
taxes.map((t, i) => {
|
||||
let margin_left = '';
|
||||
if (i !== 0) margin_left = 'ml-2';
|
||||
return `<span class="border-grey p-1 pl-2 pr-2 rounded ${margin_left}">${t.description}</span>`
|
||||
}).join('')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
} else {
|
||||
this.$totals_section.find('.taxes').html('')
|
||||
}
|
||||
}
|
||||
|
||||
get_cart_item({ item_code, batch_no, uom }) {
|
||||
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
|
||||
const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
|
||||
const uom_attr = `[data-uom=${escape(uom)}]`;
|
||||
|
||||
const item_selector = batch_no ?
|
||||
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
|
||||
|
||||
return this.$cart_items_wrapper.find(item_selector);
|
||||
}
|
||||
|
||||
update_item_html(item, remove_item) {
|
||||
const $item = this.get_cart_item(item);
|
||||
|
||||
if (remove_item) {
|
||||
$item && $item.remove();
|
||||
} else {
|
||||
const { item_code, batch_no, uom } = item;
|
||||
const search_field = batch_no ? 'batch_no' : 'item_code';
|
||||
const search_value = batch_no || item_code;
|
||||
const item_row = this.events.get_frm().doc.items.find(i => i[search_field] === search_value && i.uom === uom);
|
||||
|
||||
this.render_cart_item(item_row, $item);
|
||||
}
|
||||
|
||||
const no_of_cart_items = this.$cart_items_wrapper.children().length;
|
||||
no_of_cart_items > 0 && this.highlight_checkout_btn(no_of_cart_items > 0);
|
||||
|
||||
this.update_empty_cart_section(no_of_cart_items);
|
||||
}
|
||||
|
||||
render_cart_item(item_data, $item_to_update) {
|
||||
const currency = this.events.get_frm().doc.currency;
|
||||
const me = this;
|
||||
|
||||
if (!$item_to_update.length) {
|
||||
this.$cart_items_wrapper.append(
|
||||
`<div class="cart-item-wrapper flex items-center h-18 pr-4 pl-4 rounded border-grey pointer no-select"
|
||||
data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
|
||||
data-batch-no="${escape(item_data.batch_no || '')}">
|
||||
</div>`
|
||||
)
|
||||
$item_to_update = this.get_cart_item(item_data);
|
||||
}
|
||||
|
||||
$item_to_update.html(
|
||||
`<div class="flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap">
|
||||
<div class="text-md text-dark-grey text-bold">
|
||||
${item_data.item_name}
|
||||
</div>
|
||||
${get_description_html()}
|
||||
</div>
|
||||
${get_rate_discount_html()}
|
||||
</div>`
|
||||
)
|
||||
|
||||
set_dynamic_rate_header_width();
|
||||
this.scroll_to_item($item_to_update);
|
||||
|
||||
function set_dynamic_rate_header_width() {
|
||||
const rate_cols = Array.from(me.$cart_items_wrapper.find(".rate-col"));
|
||||
me.$cart_header.find(".rate-list-header").css("width", "");
|
||||
me.$cart_items_wrapper.find(".rate-col").css("width", "");
|
||||
let max_width = rate_cols.reduce((max_width, elm) => {
|
||||
if ($(elm).width() > max_width)
|
||||
max_width = $(elm).width();
|
||||
return max_width;
|
||||
}, 0);
|
||||
|
||||
max_width += 1;
|
||||
if (max_width == 1) max_width = "";
|
||||
|
||||
me.$cart_header.find(".rate-list-header").css("width", max_width);
|
||||
me.$cart_items_wrapper.find(".rate-col").css("width", max_width);
|
||||
}
|
||||
|
||||
function get_rate_discount_html() {
|
||||
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
|
||||
return `
|
||||
<div class="flex f-shrink-0 ml-4 items-center">
|
||||
<div class="flex w-8 h-8 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
|
||||
<span>${item_data.qty || 0}</span>
|
||||
</div>
|
||||
<div class="rate-col flex flex-col f-shrink-0 text-right">
|
||||
<div class="text-md text-dark-grey text-bold">${format_currency(item_data.amount, currency)}</div>
|
||||
<div class="text-md-0 text-dark-grey">${format_currency(item_data.rate, currency)}</div>
|
||||
</div>
|
||||
</div>`
|
||||
} else {
|
||||
return `
|
||||
<div class="flex f-shrink-0 ml-4 text-right">
|
||||
<div class="flex w-8 h-8 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
|
||||
<span>${item_data.qty || 0}</span>
|
||||
</div>
|
||||
<div class="rate-col flex flex-col f-shrink-0 text-right">
|
||||
<div class="text-md text-dark-grey text-bold">${format_currency(item_data.rate, currency)}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
function get_description_html() {
|
||||
if (item_data.description) {
|
||||
if (item_data.description.indexOf('<div>') != -1) {
|
||||
try {
|
||||
item_data.description = $(item_data.description).text();
|
||||
} catch (error) {
|
||||
item_data.description = item_data.description.replace(/<div>/g, ' ').replace(/<\/div>/g, ' ').replace(/ +/g, ' ');
|
||||
}
|
||||
}
|
||||
item_data.description = frappe.ellipsis(item_data.description, 45);
|
||||
return `<div class="text-grey">${item_data.description}</div>`
|
||||
}
|
||||
return ``;
|
||||
}
|
||||
}
|
||||
|
||||
scroll_to_item($item) {
|
||||
if ($item.length === 0) return;
|
||||
const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop();
|
||||
this.$cart_items_wrapper.animate({ scrollTop });
|
||||
}
|
||||
|
||||
update_selector_value_in_cart_item(selector, value, item) {
|
||||
const $item_to_update = this.get_cart_item(item);
|
||||
$item_to_update.attr(`data-${selector}`, value);
|
||||
}
|
||||
|
||||
toggle_checkout_btn(show_checkout) {
|
||||
if (show_checkout) {
|
||||
this.$totals_section.find('.checkout-btn').removeClass('d-none');
|
||||
this.$totals_section.find('.edit-cart-btn').addClass('d-none');
|
||||
} else {
|
||||
this.$totals_section.find('.checkout-btn').addClass('d-none');
|
||||
this.$totals_section.find('.edit-cart-btn').removeClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
highlight_checkout_btn(toggle) {
|
||||
const has_primary_class = this.$totals_section.find('.checkout-btn').hasClass('bg-primary');
|
||||
if (toggle && !has_primary_class) {
|
||||
this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg');
|
||||
} else if (!toggle && has_primary_class) {
|
||||
this.$totals_section.find('.checkout-btn').removeClass('bg-primary text-white text-lg');
|
||||
}
|
||||
}
|
||||
|
||||
update_empty_cart_section(no_of_cart_items) {
|
||||
const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper');
|
||||
|
||||
// if cart has items and no item is present
|
||||
no_of_cart_items > 0 && $no_item_element && $no_item_element.remove()
|
||||
&& this.$cart_items_wrapper.removeClass('mt-4 border-grey border-dashed') && this.$cart_header.removeClass('d-none');
|
||||
|
||||
no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder();
|
||||
}
|
||||
|
||||
on_numpad_event($btn) {
|
||||
const current_action = $btn.attr('data-button-value');
|
||||
const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action);
|
||||
|
||||
this.highlight_numpad_btn($btn, current_action);
|
||||
|
||||
const action_is_pressed_twice = this.prev_action === current_action;
|
||||
const first_click_event = !this.prev_action;
|
||||
const field_to_edit_changed = this.prev_action && this.prev_action != current_action;
|
||||
|
||||
if (action_is_field_edit) {
|
||||
|
||||
if (first_click_event || field_to_edit_changed) {
|
||||
this.prev_action = current_action;
|
||||
} else if (action_is_pressed_twice) {
|
||||
this.prev_action = undefined;
|
||||
}
|
||||
this.numpad_value = '';
|
||||
|
||||
} else if (current_action === 'checkout') {
|
||||
this.prev_action = undefined;
|
||||
this.toggle_item_highlight();
|
||||
this.events.numpad_event(undefined, current_action);
|
||||
return;
|
||||
} else if (current_action === 'remove') {
|
||||
this.prev_action = undefined;
|
||||
this.toggle_item_highlight();
|
||||
this.events.numpad_event(undefined, current_action);
|
||||
return;
|
||||
} else {
|
||||
this.numpad_value = current_action === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + current_action;
|
||||
this.numpad_value = this.numpad_value || 0;
|
||||
}
|
||||
|
||||
const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event;
|
||||
|
||||
if (first_click_event_is_not_field_edit) {
|
||||
frappe.show_alert({
|
||||
indicator: 'red',
|
||||
message: __('Please select a field to edit from numpad')
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (flt(this.numpad_value) > 100 && this.prev_action === 'discount_percentage') {
|
||||
frappe.show_alert({
|
||||
message: __('Discount cannot be greater than 100%'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
this.numpad_value = current_action;
|
||||
}
|
||||
|
||||
this.events.numpad_event(this.numpad_value, this.prev_action);
|
||||
}
|
||||
|
||||
highlight_numpad_btn($btn, curr_action) {
|
||||
const curr_action_is_highlighted = $btn.hasClass('shadow-inner');
|
||||
const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action);
|
||||
|
||||
if (!curr_action_is_highlighted) {
|
||||
$btn.addClass('shadow-inner bg-selected');
|
||||
}
|
||||
if (this.prev_action === curr_action && curr_action_is_highlighted) {
|
||||
// if Qty is pressed twice
|
||||
$btn.removeClass('shadow-inner bg-selected');
|
||||
}
|
||||
if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) {
|
||||
// Order: Qty -> Rate then remove Qty highlight
|
||||
const prev_btn = $(`[data-button-value='${this.prev_action}']`);
|
||||
prev_btn.removeClass('shadow-inner bg-selected');
|
||||
}
|
||||
if (!curr_action_is_action || curr_action === 'done') {
|
||||
// if numbers are clicked
|
||||
setTimeout(() => {
|
||||
$btn.removeClass('shadow-inner bg-selected');
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
toggle_numpad(show) {
|
||||
if (show) {
|
||||
this.$totals_section.addClass('d-none');
|
||||
this.$numpad_section.removeClass('d-none');
|
||||
} else {
|
||||
this.$totals_section.removeClass('d-none');
|
||||
this.$numpad_section.addClass('d-none');
|
||||
}
|
||||
this.reset_numpad();
|
||||
}
|
||||
|
||||
reset_numpad() {
|
||||
this.numpad_value = '';
|
||||
this.prev_action = undefined;
|
||||
this.$numpad_section.find('.shadow-inner').removeClass('shadow-inner bg-selected');
|
||||
}
|
||||
|
||||
toggle_numpad_field_edit(fieldname) {
|
||||
if (['qty', 'discount_percentage', 'rate'].includes(fieldname)) {
|
||||
this.$numpad_section.find(`[data-button-value="${fieldname}"]`).click();
|
||||
}
|
||||
}
|
||||
|
||||
toggle_customer_info(show) {
|
||||
if (show) {
|
||||
this.$cart_container.addClass('d-none')
|
||||
this.$customer_section.addClass('flex-1 scroll-y').removeClass('mb-0 border pr-4 pl-4')
|
||||
this.$customer_section.find('.icon').addClass('w-24 h-24 text-2xl').removeClass('w-12 h-12 text-md')
|
||||
this.$customer_section.find('.customer-header').removeClass('h-18');
|
||||
this.$customer_section.find('.customer-details').addClass('sticky z-100 bg-white');
|
||||
|
||||
this.$customer_section.find('.customer-name').html(
|
||||
`<div class="text-md text-dark-grey text-bold">${this.customer_info.customer}</div>
|
||||
<div class="last-transacted-on text-grey-200"></div>`
|
||||
)
|
||||
|
||||
this.$customer_section.find('.customer-details').append(
|
||||
`<div class="customer-form">
|
||||
<div class="text-grey mt-4 mb-6">CONTACT DETAILS</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="email_id-field"></div>
|
||||
<div class="mobile_no-field"></div>
|
||||
<div class="loyalty_program-field"></div>
|
||||
<div class="loyalty_points-field"></div>
|
||||
</div>
|
||||
<div class="text-grey mt-4 mb-6">RECENT TRANSACTIONS</div>
|
||||
</div>`
|
||||
)
|
||||
// transactions need to be in diff div from sticky elem for scrolling
|
||||
this.$customer_section.append(`<div class="customer-transactions flex-1 rounded"></div>`)
|
||||
|
||||
this.render_customer_info_form();
|
||||
this.fetch_customer_transactions();
|
||||
|
||||
} else {
|
||||
this.$cart_container.removeClass('d-none');
|
||||
this.$customer_section.removeClass('flex-1 scroll-y').addClass('mb-0 border pr-4 pl-4');
|
||||
this.$customer_section.find('.icon').addClass('w-12 h-12 text-md').removeClass('w-24 h-24 text-2xl');
|
||||
this.$customer_section.find('.customer-header').addClass('h-18')
|
||||
this.$customer_section.find('.customer-details').removeClass('sticky z-100 bg-white');
|
||||
|
||||
this.update_customer_section();
|
||||
}
|
||||
}
|
||||
|
||||
render_customer_info_form() {
|
||||
const $customer_form = this.$customer_section.find('.customer-form');
|
||||
|
||||
const dfs = [{
|
||||
fieldname: 'email_id',
|
||||
label: __('Email'),
|
||||
fieldtype: 'Data',
|
||||
options: 'email',
|
||||
placeholder: __("Enter customer's email")
|
||||
},{
|
||||
fieldname: 'mobile_no',
|
||||
label: __('Phone Number'),
|
||||
fieldtype: 'Data',
|
||||
placeholder: __("Enter customer's phone number")
|
||||
},{
|
||||
fieldname: 'loyalty_program',
|
||||
label: __('Loyalty Program'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Loyalty Program',
|
||||
placeholder: __("Select Loyalty Program")
|
||||
},{
|
||||
fieldname: 'loyalty_points',
|
||||
label: __('Loyalty Points'),
|
||||
fieldtype: 'Int',
|
||||
read_only: 1
|
||||
}];
|
||||
|
||||
const me = this;
|
||||
dfs.forEach(df => {
|
||||
this[`customer_${df.fieldname}_field`] = frappe.ui.form.make_control({
|
||||
df: { ...df,
|
||||
onchange: handle_customer_field_change,
|
||||
},
|
||||
parent: $customer_form.find(`.${df.fieldname}-field`),
|
||||
render_input: true,
|
||||
});
|
||||
this[`customer_${df.fieldname}_field`].set_value(this.customer_info[df.fieldname]);
|
||||
})
|
||||
|
||||
function handle_customer_field_change() {
|
||||
const current_value = me.customer_info[this.df.fieldname];
|
||||
const current_customer = me.customer_info.customer;
|
||||
|
||||
if (this.value && current_value != this.value && this.df.fieldname != 'loyalty_points') {
|
||||
frappe.call({
|
||||
method: 'erpnext.selling.page.point_of_sale.point_of_sale.set_customer_info',
|
||||
args: {
|
||||
fieldname: this.df.fieldname,
|
||||
customer: current_customer,
|
||||
value: this.value
|
||||
},
|
||||
callback: (r) => {
|
||||
if(!r.exc) {
|
||||
me.customer_info[this.df.fieldname] = this.value;
|
||||
frappe.show_alert({
|
||||
message: __("Customer contact updated successfully."),
|
||||
indicator: 'green'
|
||||
});
|
||||
frappe.utils.play_sound("submit");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch_customer_transactions() {
|
||||
frappe.db.get_list('POS Invoice', {
|
||||
filters: { customer: this.customer_info.customer, docstatus: 1 },
|
||||
fields: ['name', 'grand_total', 'status', 'posting_date', 'posting_time', 'currency'],
|
||||
limit: 20
|
||||
}).then((res) => {
|
||||
const transaction_container = this.$customer_section.find('.customer-transactions');
|
||||
|
||||
if (!res.length) {
|
||||
transaction_container.removeClass('flex-1 border rounded').html(
|
||||
`<div class="text-grey text-center">No recent transactions found</div>`
|
||||
)
|
||||
return;
|
||||
};
|
||||
|
||||
const elapsed_time = moment(res[0].posting_date+" "+res[0].posting_time).fromNow();
|
||||
this.$customer_section.find('.last-transacted-on').html(`Last transacted ${elapsed_time}`);
|
||||
|
||||
res.forEach(invoice => {
|
||||
const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma");
|
||||
let indicator_color = '';
|
||||
|
||||
if (in_list(['Paid', 'Consolidated'], invoice.status)) (indicator_color = 'green');
|
||||
if (invoice.status === 'Draft') (indicator_color = 'red');
|
||||
if (invoice.status === 'Return') (indicator_color = 'grey');
|
||||
|
||||
transaction_container.append(
|
||||
`<div class="invoice-wrapper flex p-3 justify-between border-grey rounded pointer no-select" data-invoice-name="${escape(invoice.name)}">
|
||||
<div class="flex flex-col justify-end">
|
||||
<div class="text-dark-grey text-bold overflow-hidden whitespace-nowrap mb-2">${invoice.name}</div>
|
||||
<div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
|
||||
${posting_datetime}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<div class="f-shrink-0 text-md text-dark-grey text-bold ml-4">
|
||||
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
|
||||
</div>
|
||||
<div class="f-shrink-0 text-grey ml-4 text-bold indicator ${indicator_color}">${invoice.status}</div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
load_invoice() {
|
||||
const frm = this.events.get_frm();
|
||||
this.fetch_customer_details(frm.doc.customer).then(() => {
|
||||
this.events.customer_details_updated(this.customer_info);
|
||||
this.update_customer_section();
|
||||
})
|
||||
|
||||
this.$cart_items_wrapper.html('');
|
||||
if (frm.doc.items.length) {
|
||||
frm.doc.items.forEach(item => {
|
||||
this.update_item_html(item);
|
||||
});
|
||||
} else {
|
||||
this.make_no_items_placeholder();
|
||||
this.highlight_checkout_btn(false);
|
||||
}
|
||||
|
||||
this.update_totals_section(frm);
|
||||
|
||||
if(frm.doc.docstatus === 1) {
|
||||
this.$totals_section.find('.checkout-btn').addClass('d-none');
|
||||
this.$totals_section.find('.edit-cart-btn').addClass('d-none');
|
||||
this.$totals_section.find('.grand-total').removeClass('border-b-grey');
|
||||
} else {
|
||||
this.$totals_section.find('.checkout-btn').removeClass('d-none');
|
||||
this.$totals_section.find('.edit-cart-btn').addClass('d-none');
|
||||
this.$totals_section.find('.grand-total').addClass('border-b-grey');
|
||||
}
|
||||
|
||||
this.toggle_component(true);
|
||||
}
|
||||
|
||||
toggle_component(show) {
|
||||
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
|
||||
}
|
||||
|
||||
}
|
||||
394
erpnext/selling/page/point_of_sale/pos_item_details.js
Normal file
394
erpnext/selling/page/point_of_sale/pos_item_details.js
Normal file
@@ -0,0 +1,394 @@
|
||||
erpnext.PointOfSale.ItemDetails = class {
|
||||
constructor({ wrapper, events }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
this.current_item = {};
|
||||
|
||||
this.init_component();
|
||||
}
|
||||
|
||||
init_component() {
|
||||
this.prepare_dom();
|
||||
this.init_child_components();
|
||||
this.bind_events();
|
||||
this.attach_shortcuts();
|
||||
}
|
||||
|
||||
prepare_dom() {
|
||||
this.wrapper.append(
|
||||
`<section class="col-span-4 flex shadow rounded item-details bg-white mx-h-70 h-100 d-none"></section>`
|
||||
)
|
||||
|
||||
this.$component = this.wrapper.find('.item-details');
|
||||
}
|
||||
|
||||
init_child_components() {
|
||||
this.$component.html(
|
||||
`<div class="details-container flex flex-col p-8 rounded w-full">
|
||||
<div class="flex justify-between mb-2">
|
||||
<div class="text-grey">ITEM DETAILS</div>
|
||||
<div class="close-btn text-grey hover-underline pointer no-select">Close</div>
|
||||
</div>
|
||||
<div class="item-defaults flex">
|
||||
<div class="flex-1 flex flex-col justify-end mr-4 mb-2">
|
||||
<div class="item-name text-xl font-weight-450"></div>
|
||||
<div class="item-description text-md-0 text-grey-200"></div>
|
||||
<div class="item-price text-xl font-bold"></div>
|
||||
</div>
|
||||
<div class="item-image flex items-center justify-center w-46 h-46 bg-light-grey rounded ml-4 text-6xl text-grey-100"></div>
|
||||
</div>
|
||||
<div class="discount-section flex items-center"></div>
|
||||
<div class="text-grey mt-4 mb-6">STOCK DETAILS</div>
|
||||
<div class="form-container grid grid-cols-2 row-gap-2 col-gap-4 grid-auto-row"></div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this.$item_name = this.$component.find('.item-name');
|
||||
this.$item_description = this.$component.find('.item-description');
|
||||
this.$item_price = this.$component.find('.item-price');
|
||||
this.$item_image = this.$component.find('.item-image');
|
||||
this.$form_container = this.$component.find('.form-container');
|
||||
this.$dicount_section = this.$component.find('.discount-section');
|
||||
}
|
||||
|
||||
toggle_item_details_section(item) {
|
||||
const { item_code, batch_no, uom } = this.current_item;
|
||||
const item_code_is_same = item && item_code === item.item_code;
|
||||
const batch_is_same = item && batch_no == item.batch_no;
|
||||
const uom_is_same = item && uom === item.uom;
|
||||
|
||||
this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true;
|
||||
|
||||
this.events.toggle_item_selector(this.item_has_changed);
|
||||
this.toggle_component(this.item_has_changed);
|
||||
|
||||
if (this.item_has_changed) {
|
||||
this.doctype = item.doctype;
|
||||
this.item_meta = frappe.get_meta(this.doctype);
|
||||
this.name = item.name;
|
||||
this.item_row = item;
|
||||
this.currency = this.events.get_frm().doc.currency;
|
||||
|
||||
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom };
|
||||
|
||||
this.render_dom(item);
|
||||
this.render_discount_dom(item);
|
||||
this.render_form(item);
|
||||
} else {
|
||||
this.validate_serial_batch_item();
|
||||
this.current_item = {};
|
||||
}
|
||||
}
|
||||
|
||||
validate_serial_batch_item() {
|
||||
const doc = this.events.get_frm().doc;
|
||||
const item_row = doc.items.find(item => item.name === this.name);
|
||||
|
||||
if (!item_row) return;
|
||||
|
||||
const serialized = item_row.has_serial_no;
|
||||
const batched = item_row.has_batch_no;
|
||||
const no_serial_selected = !item_row.serial_no;
|
||||
const no_batch_selected = !item_row.batch_no;
|
||||
|
||||
if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
|
||||
(serialized && batched && (no_batch_selected || no_serial_selected))) {
|
||||
|
||||
frappe.show_alert({
|
||||
message: __("Item will be removed since no serial / batch no selected."),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("cancel");
|
||||
this.events.remove_item_from_cart();
|
||||
}
|
||||
}
|
||||
|
||||
render_dom(item) {
|
||||
let { item_code ,item_name, description, image, price_list_rate } = item;
|
||||
|
||||
function get_description_html() {
|
||||
if (description) {
|
||||
description = description.indexOf('...') === -1 && description.length > 75 ? description.substr(0, 73) + '...' : description;
|
||||
return description;
|
||||
}
|
||||
return ``;
|
||||
}
|
||||
|
||||
this.$item_name.html(item_name);
|
||||
this.$item_description.html(get_description_html());
|
||||
this.$item_price.html(format_currency(price_list_rate, this.currency));
|
||||
if (image) {
|
||||
this.$item_image.html(
|
||||
`<img class="h-full" src="${image}" alt="${image}" style="object-fit: cover;">`
|
||||
);
|
||||
} else {
|
||||
this.$item_image.html(frappe.get_abbr(item_code));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render_discount_dom(item) {
|
||||
if (item.discount_percentage) {
|
||||
this.$dicount_section.html(
|
||||
`<div class="text-grey line-through mr-4 text-md mb-2">
|
||||
${format_currency(item.price_list_rate, this.currency)}
|
||||
</div>
|
||||
<div class="p-1 pr-3 pl-3 rounded w-fit text-bold bg-green-200 mb-2">
|
||||
${item.discount_percentage}% off
|
||||
</div>`
|
||||
)
|
||||
this.$item_price.html(format_currency(item.rate, this.currency));
|
||||
} else {
|
||||
this.$dicount_section.html(``)
|
||||
}
|
||||
}
|
||||
|
||||
render_form(item) {
|
||||
const fields_to_display = this.get_form_fields(item);
|
||||
this.$form_container.html('');
|
||||
|
||||
fields_to_display.forEach((fieldname, idx) => {
|
||||
this.$form_container.append(
|
||||
`<div class="">
|
||||
<div class="item_detail_field ${fieldname}-control" data-fieldname="${fieldname}"></div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname);
|
||||
fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : '';
|
||||
const me = this;
|
||||
|
||||
this[`${fieldname}_control`] = frappe.ui.form.make_control({
|
||||
df: {
|
||||
...field_meta,
|
||||
onchange: function() {
|
||||
me.events.form_updated(me.doctype, me.name, fieldname, this.value);
|
||||
}
|
||||
},
|
||||
parent: this.$form_container.find(`.${fieldname}-control`),
|
||||
render_input: true,
|
||||
})
|
||||
this[`${fieldname}_control`].set_value(item[fieldname]);
|
||||
});
|
||||
|
||||
this.make_auto_serial_selection_btn(item);
|
||||
|
||||
this.bind_custom_control_change_event();
|
||||
}
|
||||
|
||||
get_form_fields(item) {
|
||||
const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty'];
|
||||
if (item.has_serial_no) fields.push('serial_no');
|
||||
if (item.has_batch_no) fields.push('batch_no');
|
||||
return fields;
|
||||
}
|
||||
|
||||
make_auto_serial_selection_btn(item) {
|
||||
if (item.has_serial_no) {
|
||||
this.$form_container.append(
|
||||
`<div class="grid-filler no-select"></div>`
|
||||
)
|
||||
if (!item.has_batch_no) {
|
||||
this.$form_container.append(
|
||||
`<div class="grid-filler no-select"></div>`
|
||||
)
|
||||
}
|
||||
this.$form_container.append(
|
||||
`<div class="auto-fetch-btn bg-grey-100 border border-grey text-bold rounded pt-3 pb-3 pl-6 pr-8 text-grey pointer no-select mt-2"
|
||||
style="height: 3.3rem">
|
||||
Auto Fetch Serial Numbers
|
||||
</div>`
|
||||
)
|
||||
this.$form_container.find('.serial_no-control').find('textarea').css('height', '9rem');
|
||||
this.$form_container.find('.serial_no-control').parent().addClass('row-span-2');
|
||||
}
|
||||
}
|
||||
|
||||
bind_custom_control_change_event() {
|
||||
const me = this;
|
||||
if (this.rate_control) {
|
||||
this.rate_control.df.onchange = function() {
|
||||
if (this.value) {
|
||||
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
const doc = me.events.get_frm().doc;
|
||||
|
||||
me.$item_price.html(format_currency(item_row.rate, doc.currency));
|
||||
me.render_discount_dom(item_row);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.warehouse_control) {
|
||||
this.warehouse_control.df.reqd = 1;
|
||||
this.warehouse_control.df.onchange = function() {
|
||||
if (this.value) {
|
||||
me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => {
|
||||
me.item_stock_map = me.events.get_item_stock_map();
|
||||
const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
|
||||
if (available_qty === undefined) {
|
||||
me.events.get_available_stock(me.item_row.item_code, this.value).then(() => {
|
||||
// item stock map is updated now reset warehouse
|
||||
me.warehouse_control.set_value(this.value);
|
||||
})
|
||||
} else if (available_qty === 0) {
|
||||
me.warehouse_control.set_value('');
|
||||
frappe.throw(__(`Item Code: ${me.item_row.item_code.bold()} is not available under warehouse ${this.value.bold()}.`));
|
||||
}
|
||||
me.actual_qty_control.set_value(available_qty);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.warehouse_control.refresh();
|
||||
}
|
||||
|
||||
if (this.discount_percentage_control) {
|
||||
this.discount_percentage_control.df.onchange = function() {
|
||||
if (this.value) {
|
||||
me.events.form_updated(me.doctype, me.name, 'discount_percentage', this.value).then(() => {
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
me.rate_control.set_value(item_row.rate);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.serial_no_control) {
|
||||
this.serial_no_control.df.reqd = 1;
|
||||
this.serial_no_control.df.onchange = async function() {
|
||||
!me.current_item.batch_no && await me.auto_update_batch_no();
|
||||
me.events.form_updated(me.doctype, me.name, 'serial_no', this.value);
|
||||
}
|
||||
this.serial_no_control.refresh();
|
||||
}
|
||||
|
||||
if (this.batch_no_control) {
|
||||
this.batch_no_control.df.reqd = 1;
|
||||
this.batch_no_control.df.get_query = () => {
|
||||
return {
|
||||
query: 'erpnext.controllers.queries.get_batch_no',
|
||||
filters: {
|
||||
item_code: me.item_row.item_code,
|
||||
warehouse: me.item_row.warehouse
|
||||
}
|
||||
}
|
||||
};
|
||||
this.batch_no_control.df.onchange = function() {
|
||||
me.events.set_value_in_current_cart_item('batch-no', this.value);
|
||||
me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
|
||||
me.current_item.batch_no = this.value;
|
||||
}
|
||||
this.batch_no_control.refresh();
|
||||
}
|
||||
|
||||
if (this.uom_control) {
|
||||
this.uom_control.df.onchange = function() {
|
||||
me.events.set_value_in_current_cart_item('uom', this.value);
|
||||
me.events.form_updated(me.doctype, me.name, 'uom', this.value);
|
||||
me.current_item.uom = this.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async auto_update_batch_no() {
|
||||
if (this.serial_no_control && this.batch_no_control) {
|
||||
const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s);
|
||||
if (!selected_serial_nos.length) return;
|
||||
|
||||
// find batch nos of the selected serial no
|
||||
const serials_with_batch_no = await frappe.db.get_list("Serial No", {
|
||||
filters: { 'name': ["in", selected_serial_nos]},
|
||||
fields: ["batch_no", "name"]
|
||||
});
|
||||
const batch_serial_map = serials_with_batch_no.reduce((acc, r) => {
|
||||
acc[r.batch_no] || (acc[r.batch_no] = []);
|
||||
acc[r.batch_no] = [...acc[r.batch_no], r.name];
|
||||
return acc;
|
||||
}, {});
|
||||
// set current item's batch no and serial no
|
||||
const batch_no = Object.keys(batch_serial_map)[0];
|
||||
const batch_serial_nos = batch_serial_map[batch_no].join(`\n`);
|
||||
// eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch
|
||||
const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length;
|
||||
|
||||
const current_batch_no = this.batch_no_control.get_value();
|
||||
current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no);
|
||||
|
||||
if (serial_nos_belongs_to_other_batch) {
|
||||
this.serial_no_control.set_value(batch_serial_nos);
|
||||
this.qty_control.set_value(batch_serial_map[batch_no].length);
|
||||
}
|
||||
|
||||
delete batch_serial_map[batch_no];
|
||||
|
||||
if (serial_nos_belongs_to_other_batch)
|
||||
this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
|
||||
}
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this.bind_auto_serial_fetch_event();
|
||||
this.bind_fields_to_numpad_fields();
|
||||
|
||||
this.$component.on('click', '.close-btn', () => {
|
||||
this.events.close_item_details();
|
||||
});
|
||||
}
|
||||
|
||||
attach_shortcuts() {
|
||||
frappe.ui.keys.on("escape", () => {
|
||||
const item_details_visible = this.$component.is(":visible");
|
||||
if (item_details_visible) {
|
||||
this.events.close_item_details();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bind_fields_to_numpad_fields() {
|
||||
const me = this;
|
||||
this.$form_container.on('click', '.input-with-feedback', function() {
|
||||
const fieldname = $(this).attr('data-fieldname');
|
||||
if (this.last_field_focused != fieldname) {
|
||||
me.events.item_field_focused(fieldname);
|
||||
this.last_field_focused = fieldname;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bind_auto_serial_fetch_event() {
|
||||
this.$form_container.on('click', '.auto-fetch-btn', () => {
|
||||
this.batch_no_control.set_value('');
|
||||
let qty = this.qty_control.get_value();
|
||||
let numbers = frappe.call({
|
||||
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
|
||||
args: {
|
||||
qty,
|
||||
item_code: this.current_item.item_code,
|
||||
warehouse: this.warehouse_control.get_value() || '',
|
||||
batch_nos: this.current_item.batch_no || '',
|
||||
for_doctype: 'POS Invoice'
|
||||
}
|
||||
});
|
||||
|
||||
numbers.then((data) => {
|
||||
let auto_fetched_serial_numbers = data.message;
|
||||
let records_length = auto_fetched_serial_numbers.length;
|
||||
if (!records_length) {
|
||||
const warehouse = this.warehouse_control.get_value().bold();
|
||||
frappe.msgprint(__(`Serial numbers unavailable for Item ${this.current_item.item_code.bold()}
|
||||
under warehouse ${warehouse}. Please try changing warehouse.`));
|
||||
} else if (records_length < qty) {
|
||||
frappe.msgprint(`Fetched only ${records_length} available serial numbers.`);
|
||||
this.qty_control.set_value(records_length);
|
||||
}
|
||||
numbers = auto_fetched_serial_numbers.join(`\n`);
|
||||
this.serial_no_control.set_value(numbers);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
toggle_component(show) {
|
||||
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
|
||||
}
|
||||
}
|
||||
265
erpnext/selling/page/point_of_sale/pos_item_selector.js
Normal file
265
erpnext/selling/page/point_of_sale/pos_item_selector.js
Normal file
@@ -0,0 +1,265 @@
|
||||
erpnext.PointOfSale.ItemSelector = class {
|
||||
constructor({ frm, wrapper, events, pos_profile }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
this.pos_profile = pos_profile;
|
||||
|
||||
this.inti_component();
|
||||
}
|
||||
|
||||
inti_component() {
|
||||
this.prepare_dom();
|
||||
this.make_search_bar();
|
||||
this.load_items_data();
|
||||
this.bind_events();
|
||||
this.attach_shortcuts();
|
||||
}
|
||||
|
||||
prepare_dom() {
|
||||
this.wrapper.append(
|
||||
`<section class="col-span-6 flex shadow rounded items-selector bg-white mx-h-70 h-100">
|
||||
<div class="flex flex-col rounded w-full scroll-y">
|
||||
<div class="filter-section flex p-8 pb-2 bg-white sticky z-100">
|
||||
<div class="search-field flex f-grow-3 mr-8 items-center text-grey"></div>
|
||||
<div class="item-group-field flex f-grow-1 items-center text-grey text-bold"></div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col p-8 pt-2">
|
||||
<div class="text-grey mb-6">ALL ITEMS</div>
|
||||
<div class="items-container grid grid-cols-4 gap-8">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>`
|
||||
);
|
||||
|
||||
this.$component = this.wrapper.find('.items-selector');
|
||||
}
|
||||
|
||||
async load_items_data() {
|
||||
if (!this.item_group) {
|
||||
const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name");
|
||||
this.parent_item_group = res.message.name;
|
||||
};
|
||||
if (!this.price_list) {
|
||||
const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list");
|
||||
this.price_list = res.message.selling_price_list;
|
||||
}
|
||||
|
||||
this.get_items({}).then(({message}) => {
|
||||
this.render_item_list(message.items);
|
||||
});
|
||||
}
|
||||
|
||||
get_items({start = 0, page_length = 40, search_value=''}) {
|
||||
const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list;
|
||||
let { item_group, pos_profile } = this;
|
||||
|
||||
!item_group && (item_group = this.parent_item_group);
|
||||
|
||||
return frappe.call({
|
||||
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
|
||||
freeze: true,
|
||||
args: { start, page_length, price_list, item_group, search_value, pos_profile },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
render_item_list(items) {
|
||||
this.$items_container = this.$component.find('.items-container');
|
||||
this.$items_container.html('');
|
||||
|
||||
items.forEach(item => {
|
||||
const item_html = this.get_item_html(item);
|
||||
this.$items_container.append(item_html);
|
||||
})
|
||||
}
|
||||
|
||||
get_item_html(item) {
|
||||
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
|
||||
const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red";
|
||||
|
||||
function get_item_image_html() {
|
||||
if (item_image) {
|
||||
return `<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
|
||||
<img class="h-full" src="${item_image}" alt="${item_image}" style="object-fit: cover;">
|
||||
</div>`
|
||||
} else {
|
||||
return `<div class="flex items-center justify-center h-32 bg-light-grey text-6xl text-grey-100">
|
||||
${frappe.get_abbr(item.item_name)}
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
`<div class="item-wrapper rounded shadow pointer no-select" data-item-code="${escape(item.item_code)}"
|
||||
data-serial-no="${escape(serial_no)}" data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
|
||||
title="Avaiable Qty: ${actual_qty}">
|
||||
${get_item_image_html()}
|
||||
<div class="flex items-center pr-4 pl-4 h-10 justify-between">
|
||||
<div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
|
||||
<span class="indicator ${indicator_color}"></span>
|
||||
${frappe.ellipsis(item.item_name, 18)}
|
||||
</div>
|
||||
<div class="f-shrink-0 text-dark-grey text-bold ml-4">${format_currency(item.price_list_rate, item.currency, 0) || 0}</div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
|
||||
make_search_bar() {
|
||||
const me = this;
|
||||
this.$component.find('.search-field').html('');
|
||||
this.$component.find('.item-group-field').html('');
|
||||
|
||||
this.search_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Search'),
|
||||
fieldtype: 'Data',
|
||||
placeholder: __('Search by item code, serial number, batch no or barcode')
|
||||
},
|
||||
parent: this.$component.find('.search-field'),
|
||||
render_input: true,
|
||||
});
|
||||
this.item_group_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Item Group'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Item Group',
|
||||
placeholder: __('Select item group'),
|
||||
onchange: function() {
|
||||
me.item_group = this.value;
|
||||
!me.item_group && (me.item_group = me.parent_item_group);
|
||||
me.filter_items();
|
||||
},
|
||||
get_query: function () {
|
||||
return {
|
||||
query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query',
|
||||
filters: {
|
||||
pos_profile: me.events.get_frm().doc?.pos_profile
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
parent: this.$component.find('.item-group-field'),
|
||||
render_input: true,
|
||||
});
|
||||
this.search_field.toggle_label(false);
|
||||
this.item_group_field.toggle_label(false);
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
const me = this;
|
||||
onScan.attachTo(document, {
|
||||
onScan: (sScancode) => {
|
||||
if (this.search_field && this.$component.is(':visible')) {
|
||||
this.search_field.set_focus();
|
||||
$(this.search_field.$input[0]).val(sScancode).trigger("input");
|
||||
this.barcode_scanned = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$component.on('click', '.item-wrapper', function() {
|
||||
const $item = $(this);
|
||||
const item_code = unescape($item.attr('data-item-code'));
|
||||
let batch_no = unescape($item.attr('data-batch-no'));
|
||||
let serial_no = unescape($item.attr('data-serial-no'));
|
||||
let uom = unescape($item.attr('data-uom'));
|
||||
|
||||
// escape(undefined) returns "undefined" then unescape returns "undefined"
|
||||
batch_no = batch_no === "undefined" ? undefined : batch_no;
|
||||
serial_no = serial_no === "undefined" ? undefined : serial_no;
|
||||
uom = uom === "undefined" ? undefined : uom;
|
||||
|
||||
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
|
||||
})
|
||||
|
||||
this.search_field.$input.on('input', (e) => {
|
||||
clearTimeout(this.last_search);
|
||||
this.last_search = setTimeout(() => {
|
||||
const search_term = e.target.value;
|
||||
this.filter_items({ search_term });
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
attach_shortcuts() {
|
||||
frappe.ui.keys.on("ctrl+i", () => {
|
||||
const selector_is_visible = this.$component.is(':visible');
|
||||
if (!selector_is_visible) return;
|
||||
this.search_field.set_focus();
|
||||
});
|
||||
frappe.ui.keys.on("ctrl+g", () => {
|
||||
const selector_is_visible = this.$component.is(':visible');
|
||||
if (!selector_is_visible) return;
|
||||
this.item_group_field.set_focus();
|
||||
});
|
||||
// for selecting the last filtered item on search
|
||||
frappe.ui.keys.on("enter", () => {
|
||||
const selector_is_visible = this.$component.is(':visible');
|
||||
if (!selector_is_visible || this.search_field.get_value() === "") return;
|
||||
|
||||
if (this.items.length == 1) {
|
||||
this.$items_container.find(".item-wrapper").click();
|
||||
frappe.utils.play_sound("submit");
|
||||
$(this.search_field.$input[0]).val("").trigger("input");
|
||||
} else if (this.items.length == 0 && this.barcode_scanned) {
|
||||
// only show alert of barcode is scanned and enter is pressed
|
||||
frappe.show_alert({
|
||||
message: __("No items found. Scan barcode again."),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
this.barcode_scanned = false;
|
||||
$(this.search_field.$input[0]).val("").trigger("input");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filter_items({ search_term='' }={}) {
|
||||
if (search_term) {
|
||||
search_term = search_term.toLowerCase();
|
||||
|
||||
// memoize
|
||||
this.search_index = this.search_index || {};
|
||||
if (this.search_index[search_term]) {
|
||||
const items = this.search_index[search_term];
|
||||
this.items = items;
|
||||
this.render_item_list(items);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.get_items({ search_value: search_term })
|
||||
.then(({ message }) => {
|
||||
const { items, serial_no, batch_no, barcode } = message;
|
||||
if (search_term && !barcode) {
|
||||
this.search_index[search_term] = items;
|
||||
}
|
||||
this.items = items;
|
||||
this.render_item_list(items);
|
||||
});
|
||||
}
|
||||
|
||||
resize_selector(minimize) {
|
||||
minimize ?
|
||||
this.$component.find('.search-field').removeClass('mr-8') :
|
||||
this.$component.find('.search-field').addClass('mr-8');
|
||||
|
||||
minimize ?
|
||||
this.$component.find('.filter-section').addClass('flex-col') :
|
||||
this.$component.find('.filter-section').removeClass('flex-col');
|
||||
|
||||
minimize ?
|
||||
this.$component.removeClass('col-span-6').addClass('col-span-2') :
|
||||
this.$component.removeClass('col-span-2').addClass('col-span-6')
|
||||
|
||||
minimize ?
|
||||
this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') :
|
||||
this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4')
|
||||
}
|
||||
|
||||
toggle_component(show) {
|
||||
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
|
||||
}
|
||||
}
|
||||
49
erpnext/selling/page/point_of_sale/pos_number_pad.js
Normal file
49
erpnext/selling/page/point_of_sale/pos_number_pad.js
Normal file
@@ -0,0 +1,49 @@
|
||||
erpnext.PointOfSale.NumberPad = class {
|
||||
constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
this.cols = cols;
|
||||
this.keys = keys;
|
||||
this.css_classes = css_classes || [];
|
||||
this.fieldnames = fieldnames_map || {};
|
||||
|
||||
this.init_component();
|
||||
}
|
||||
|
||||
init_component() {
|
||||
this.prepare_dom();
|
||||
this.bind_events();
|
||||
}
|
||||
|
||||
prepare_dom() {
|
||||
const { cols, keys, css_classes, fieldnames } = this;
|
||||
|
||||
function get_keys() {
|
||||
return keys.reduce((a, row, i) => {
|
||||
return a + row.reduce((a2, number, j) => {
|
||||
const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : '';
|
||||
const fieldname = fieldnames && fieldnames[number] ?
|
||||
fieldnames[number] :
|
||||
typeof number === 'string' ? frappe.scrub(number) : number;
|
||||
|
||||
return a2 + `<div class="numpad-btn pointer no-select rounded ${class_to_append}
|
||||
flex items-center justify-center h-16 text-md border-grey border" data-button-value="${fieldname}">${number}</div>`
|
||||
}, '')
|
||||
}, '');
|
||||
}
|
||||
|
||||
this.wrapper.html(
|
||||
`<div class="grid grid-cols-${cols} gap-4">
|
||||
${get_keys()}
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
const me = this;
|
||||
this.wrapper.on('click', '.numpad-btn', function() {
|
||||
const $btn = $(this);
|
||||
me.events.numpad_event($btn);
|
||||
})
|
||||
}
|
||||
}
|
||||
130
erpnext/selling/page/point_of_sale/pos_past_order_list.js
Normal file
130
erpnext/selling/page/point_of_sale/pos_past_order_list.js
Normal file
@@ -0,0 +1,130 @@
|
||||
erpnext.PointOfSale.PastOrderList = class {
|
||||
constructor({ wrapper, events }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
|
||||
this.init_component();
|
||||
}
|
||||
|
||||
init_component() {
|
||||
this.prepare_dom();
|
||||
this.make_filter_section();
|
||||
this.bind_events();
|
||||
}
|
||||
|
||||
prepare_dom() {
|
||||
this.wrapper.append(
|
||||
`<section class="col-span-4 flex flex-col shadow rounded past-order-list bg-white mx-h-70 h-100 d-none">
|
||||
<div class="flex flex-col rounded w-full scroll-y">
|
||||
<div class="filter-section flex flex-col p-8 pb-2 bg-white sticky z-100">
|
||||
<div class="search-field flex items-center text-grey"></div>
|
||||
<div class="status-field flex items-center text-grey text-bold"></div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col p-8 pt-2">
|
||||
<div class="text-grey mb-6">RECENT ORDERS</div>
|
||||
<div class="invoices-container rounded border grid grid-cols-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>`
|
||||
)
|
||||
|
||||
this.$component = this.wrapper.find('.past-order-list');
|
||||
this.$invoices_container = this.$component.find('.invoices-container');
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this.search_field.$input.on('input', (e) => {
|
||||
clearTimeout(this.last_search);
|
||||
this.last_search = setTimeout(() => {
|
||||
const search_term = e.target.value;
|
||||
this.refresh_list(search_term, this.status_field.get_value());
|
||||
}, 300);
|
||||
});
|
||||
const me = this;
|
||||
this.$invoices_container.on('click', '.invoice-wrapper', function() {
|
||||
const invoice_name = unescape($(this).attr('data-invoice-name'));
|
||||
|
||||
me.events.open_invoice_data(invoice_name);
|
||||
})
|
||||
}
|
||||
|
||||
make_filter_section() {
|
||||
const me = this;
|
||||
this.search_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Search'),
|
||||
fieldtype: 'Data',
|
||||
placeholder: __('Search by invoice id or customer name')
|
||||
},
|
||||
parent: this.$component.find('.search-field'),
|
||||
render_input: true,
|
||||
});
|
||||
this.status_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Invoice Status'),
|
||||
fieldtype: 'Select',
|
||||
options: `Draft\nPaid\nConsolidated\nReturn`,
|
||||
placeholder: __('Filter by invoice status'),
|
||||
onchange: function() {
|
||||
me.refresh_list(me.search_field.get_value(), this.value);
|
||||
}
|
||||
},
|
||||
parent: this.$component.find('.status-field'),
|
||||
render_input: true,
|
||||
});
|
||||
this.search_field.toggle_label(false);
|
||||
this.status_field.toggle_label(false);
|
||||
this.status_field.set_value('Paid');
|
||||
}
|
||||
|
||||
toggle_component(show) {
|
||||
show ?
|
||||
this.$component.removeClass('d-none') && this.refresh_list() :
|
||||
this.$component.addClass('d-none');
|
||||
}
|
||||
|
||||
refresh_list() {
|
||||
frappe.dom.freeze();
|
||||
this.events.reset_summary();
|
||||
const search_term = this.search_field.get_value();
|
||||
const status = this.status_field.get_value();
|
||||
|
||||
this.$invoices_container.html('');
|
||||
|
||||
return frappe.call({
|
||||
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_past_order_list",
|
||||
freeze: true,
|
||||
args: { search_term, status },
|
||||
callback: (response) => {
|
||||
frappe.dom.unfreeze();
|
||||
response.message.forEach(invoice => {
|
||||
const invoice_html = this.get_invoice_html(invoice);
|
||||
this.$invoices_container.append(invoice_html);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_invoice_html(invoice) {
|
||||
const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma");
|
||||
return (
|
||||
`<div class="invoice-wrapper flex p-4 justify-between border-b-grey pointer no-select" data-invoice-name="${escape(invoice.name)}">
|
||||
<div class="flex flex-col justify-end">
|
||||
<div class="text-dark-grey text-bold overflow-hidden whitespace-nowrap mb-2">${invoice.name}</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
|
||||
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
${invoice.customer}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<div class="f-shrink-0 text-lg text-dark-grey text-bold ml-4">${format_currency(invoice.grand_total, invoice.currency, 0) || 0}</div>
|
||||
<div class="f-shrink-0 text-grey ml-4">${posting_datetime}</div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
}
|
||||
456
erpnext/selling/page/point_of_sale/pos_past_order_summary.js
Normal file
456
erpnext/selling/page/point_of_sale/pos_past_order_summary.js
Normal file
@@ -0,0 +1,456 @@
|
||||
erpnext.PointOfSale.PastOrderSummary = class {
|
||||
constructor({ wrapper, events }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
|
||||
this.init_component();
|
||||
}
|
||||
|
||||
init_component() {
|
||||
this.prepare_dom();
|
||||
this.init_child_components();
|
||||
this.bind_events();
|
||||
this.attach_shortcuts();
|
||||
}
|
||||
|
||||
prepare_dom() {
|
||||
this.wrapper.append(
|
||||
`<section class="col-span-6 flex flex-col items-center shadow rounded past-order-summary bg-white mx-h-70 h-100 d-none">
|
||||
<div class="no-summary-placeholder flex flex-1 items-center justify-center p-16">
|
||||
<div class="no-item-wrapper flex items-center h-18 pr-4 pl-4">
|
||||
<div class="flex-1 text-center text-grey">Select an invoice to load summary data</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-wrapper d-none flex-1 w-66 text-dark-grey relative">
|
||||
<div class="summary-container absolute flex flex-col pt-16 pb-16 pr-8 pl-8 w-full h-full"></div>
|
||||
</div>
|
||||
</section>`
|
||||
)
|
||||
|
||||
this.$component = this.wrapper.find('.past-order-summary');
|
||||
this.$summary_wrapper = this.$component.find('.summary-wrapper');
|
||||
this.$summary_container = this.$component.find('.summary-container');
|
||||
}
|
||||
|
||||
init_child_components() {
|
||||
this.init_upper_section();
|
||||
this.init_items_summary();
|
||||
this.init_totals_summary();
|
||||
this.init_payments_summary();
|
||||
this.init_summary_buttons();
|
||||
this.init_email_print_dialog();
|
||||
}
|
||||
|
||||
init_upper_section() {
|
||||
this.$summary_container.append(
|
||||
`<div class="flex upper-section justify-between w-full h-24"></div>`
|
||||
);
|
||||
|
||||
this.$upper_section = this.$summary_container.find('.upper-section');
|
||||
}
|
||||
|
||||
init_items_summary() {
|
||||
this.$summary_container.append(
|
||||
`<div class="flex flex-col flex-1 mt-6 w-full scroll-y">
|
||||
<div class="text-grey mb-4 sticky bg-white">ITEMS</div>
|
||||
<div class="items-summary-container border rounded flex flex-col w-full"></div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this.$items_summary_container = this.$summary_container.find('.items-summary-container');
|
||||
}
|
||||
|
||||
init_totals_summary() {
|
||||
this.$summary_container.append(
|
||||
`<div class="flex flex-col mt-6 w-full f-shrink-0">
|
||||
<div class="text-grey mb-4">TOTALS</div>
|
||||
<div class="summary-totals-container border rounded flex flex-col w-full"></div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this.$totals_summary_container = this.$summary_container.find('.summary-totals-container');
|
||||
}
|
||||
|
||||
init_payments_summary() {
|
||||
this.$summary_container.append(
|
||||
`<div class="flex flex-col mt-6 w-full f-shrink-0">
|
||||
<div class="text-grey mb-4">PAYMENTS</div>
|
||||
<div class="payments-summary-container border rounded flex flex-col w-full mb-4"></div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this.$payment_summary_container = this.$summary_container.find('.payments-summary-container');
|
||||
}
|
||||
|
||||
init_summary_buttons() {
|
||||
this.$summary_container.append(
|
||||
`<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>`
|
||||
)
|
||||
|
||||
this.$summary_btns = this.$summary_container.find('.summary-btns');
|
||||
}
|
||||
|
||||
init_email_print_dialog() {
|
||||
const email_dialog = new frappe.ui.Dialog({
|
||||
title: 'Email Receipt',
|
||||
fields: [
|
||||
{fieldname:'email_id', fieldtype:'Data', options: 'Email', label:'Email ID'},
|
||||
// {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'}
|
||||
],
|
||||
primary_action: () => {
|
||||
this.send_email();
|
||||
},
|
||||
primary_action_label: __('Send'),
|
||||
});
|
||||
this.email_dialog = email_dialog;
|
||||
|
||||
const print_dialog = new frappe.ui.Dialog({
|
||||
title: 'Print Receipt',
|
||||
fields: [
|
||||
{fieldname:'print', fieldtype:'Data', label:'Print Preview'}
|
||||
],
|
||||
primary_action: () => {
|
||||
const frm = this.events.get_frm();
|
||||
frm.doc = this.doc;
|
||||
frm.print_preview.lang_code = frm.doc.language;
|
||||
frm.print_preview.printit(true);
|
||||
},
|
||||
primary_action_label: __('Print'),
|
||||
});
|
||||
this.print_dialog = print_dialog;
|
||||
}
|
||||
|
||||
get_upper_section_html(doc) {
|
||||
const { status } = doc; let indicator_color = '';
|
||||
|
||||
in_list(['Paid', 'Consolidated'], status) && (indicator_color = 'green');
|
||||
status === 'Draft' && (indicator_color = 'red');
|
||||
status === 'Return' && (indicator_color = 'grey');
|
||||
|
||||
return `<div class="flex flex-col items-start justify-end pr-4">
|
||||
<div class="text-lg text-bold pt-2">${doc.customer}</div>
|
||||
<div class="text-grey">${this.customer_email}</div>
|
||||
<div class="text-grey mt-auto">Sold by: ${doc.owner}</div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 items-end justify-between">
|
||||
<div class="text-2-5xl text-bold">${format_currency(doc.paid_amount, doc.currency)}</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-grey mr-4">${doc.name}</div>
|
||||
<div class="text-grey text-bold indicator ${indicator_color}">${doc.status}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
get_discount_html(doc) {
|
||||
if (doc.discount_amount) {
|
||||
return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
|
||||
<div class="flex f-shrink-1 items-center">
|
||||
<div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap mr-2">
|
||||
Discount
|
||||
</div>
|
||||
<span class="text-grey">(${doc.additional_discount_percentage} %)</span>
|
||||
</div>
|
||||
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.discount_amount, doc.currency)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
return ``;
|
||||
}
|
||||
}
|
||||
|
||||
get_net_total_html(doc) {
|
||||
return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
|
||||
<div class="flex f-shrink-1 items-center">
|
||||
<div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
|
||||
Net Total
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.net_total, doc.currency)}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
get_taxes_html(doc) {
|
||||
return `<div class="total-summary-wrapper flex items-center justify-between h-12 pr-4 pl-4 border-b-grey">
|
||||
<div class="flex">
|
||||
<div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div>
|
||||
<div class="flex ml-6 text-dark-grey">
|
||||
${
|
||||
doc.taxes.map((t, i) => {
|
||||
let margin_left = '';
|
||||
if (i !== 0) margin_left = 'ml-2';
|
||||
return `<span class="pl-2 pr-2 ${margin_left}">${t.description} @${t.rate}%</span>`
|
||||
}).join('')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.base_total_taxes_and_charges, doc.currency)}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
get_grand_total_html(doc) {
|
||||
return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
|
||||
<div class="flex f-shrink-1 items-center">
|
||||
<div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
|
||||
Grand Total
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.grand_total, doc.currency)}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
get_item_html(doc, item_data) {
|
||||
return `<div class="item-summary-wrapper flex items-center h-12 pr-4 pl-4 border-b-grey pointer no-select">
|
||||
<div class="flex w-6 h-6 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
|
||||
<span>${item_data.qty || 0}</span>
|
||||
</div>
|
||||
<div class="flex flex-col f-shrink-1">
|
||||
<div class="text-md text-dark-grey text-bold overflow-hidden whitespace-nowrap">
|
||||
${item_data.item_name}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex f-shrink-0 ml-auto text-right">
|
||||
${get_rate_discount_html()}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
function get_rate_discount_html() {
|
||||
if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) {
|
||||
return `<span class="text-grey mr-2">(${item_data.discount_percentage}% off)</span>
|
||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(item_data.rate, doc.currency)}</div>`
|
||||
} else {
|
||||
return `<div class="text-md-0 text-dark-grey text-bold">${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_payment_html(doc, payment) {
|
||||
return `<div class="payment-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
|
||||
<div class="flex f-shrink-1 items-center">
|
||||
<div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
|
||||
${payment.mode_of_payment}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col f-shrink-0 ml-auto text-right">
|
||||
<div class="text-md-0 text-dark-grey text-bold">${format_currency(payment.amount, doc.currency)}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this.$summary_container.on('click', '.return-btn', () => {
|
||||
this.events.process_return(this.doc.name);
|
||||
this.toggle_component(false);
|
||||
this.$component.find('.no-summary-placeholder').removeClass('d-none');
|
||||
this.$summary_wrapper.addClass('d-none');
|
||||
});
|
||||
|
||||
this.$summary_container.on('click', '.edit-btn', () => {
|
||||
this.events.edit_order(this.doc.name);
|
||||
this.toggle_component(false);
|
||||
this.$component.find('.no-summary-placeholder').removeClass('d-none');
|
||||
this.$summary_wrapper.addClass('d-none');
|
||||
});
|
||||
|
||||
this.$summary_container.on('click', '.new-btn', () => {
|
||||
this.events.new_order();
|
||||
this.toggle_component(false);
|
||||
this.$component.find('.no-summary-placeholder').removeClass('d-none');
|
||||
this.$summary_wrapper.addClass('d-none');
|
||||
});
|
||||
|
||||
this.$summary_container.on('click', '.email-btn', () => {
|
||||
this.email_dialog.fields_dict.email_id.set_value(this.customer_email);
|
||||
this.email_dialog.show();
|
||||
});
|
||||
|
||||
this.$summary_container.on('click', '.print-btn', () => {
|
||||
// this.print_dialog.show();
|
||||
const frm = this.events.get_frm();
|
||||
frm.doc = this.doc;
|
||||
frm.print_preview.lang_code = frm.doc.language;
|
||||
frm.print_preview.printit(true);
|
||||
});
|
||||
}
|
||||
|
||||
attach_shortcuts() {
|
||||
frappe.ui.keys.on("ctrl+p", () => {
|
||||
const print_btn_visible = this.$summary_container.find('.print-btn').is(":visible");
|
||||
const summary_visible = this.$component.is(":visible");
|
||||
if (!summary_visible || !print_btn_visible) return;
|
||||
|
||||
this.$summary_container.find('.print-btn').click();
|
||||
});
|
||||
}
|
||||
|
||||
toggle_component(show) {
|
||||
show ?
|
||||
this.$component.removeClass('d-none') :
|
||||
this.$component.addClass('d-none');
|
||||
}
|
||||
|
||||
send_email() {
|
||||
const frm = this.events.get_frm();
|
||||
const recipients = this.email_dialog.get_values().recipients;
|
||||
const doc = this.doc || frm.doc;
|
||||
const print_format = frm.pos_print_format;
|
||||
|
||||
frappe.call({
|
||||
method:"frappe.core.doctype.communication.email.make",
|
||||
args: {
|
||||
recipients: recipients,
|
||||
subject: __(frm.meta.name) + ': ' + doc.name,
|
||||
doctype: doc.doctype,
|
||||
name: doc.name,
|
||||
send_email: 1,
|
||||
print_format,
|
||||
sender_full_name: frappe.user.full_name(),
|
||||
_lang : doc.language
|
||||
},
|
||||
callback: r => {
|
||||
if(!r.exc) {
|
||||
frappe.utils.play_sound("email");
|
||||
if(r.message["emails_not_sent_to"]) {
|
||||
frappe.msgprint(__("Email not sent to {0} (unsubscribed / disabled)",
|
||||
[ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) );
|
||||
} else {
|
||||
frappe.show_alert({
|
||||
message: __('Email sent successfully.'),
|
||||
indicator: 'green'
|
||||
});
|
||||
}
|
||||
this.email_dialog.hide();
|
||||
} else {
|
||||
frappe.msgprint(__("There were errors while sending email. Please try again."));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add_summary_btns(map) {
|
||||
this.$summary_btns.html('');
|
||||
map.forEach(m => {
|
||||
if (m.condition) {
|
||||
m.visible_btns.forEach(b => {
|
||||
const class_name = b.split(' ')[0].toLowerCase();
|
||||
this.$summary_btns.append(
|
||||
`<div class="${class_name}-btn border rounded h-14 flex flex-1 items-center mr-4 justify-center text-md text-bold no-select pointer">
|
||||
${b}
|
||||
</div>`
|
||||
)
|
||||
});
|
||||
}
|
||||
});
|
||||
this.$summary_btns.children().last().removeClass('mr-4');
|
||||
}
|
||||
|
||||
show_summary_placeholder() {
|
||||
this.$summary_wrapper.addClass("d-none");
|
||||
this.$component.find('.no-summary-placeholder').removeClass('d-none');
|
||||
}
|
||||
|
||||
switch_to_post_submit_summary() {
|
||||
// switch to full width view
|
||||
this.$component.removeClass('col-span-6').addClass('col-span-10');
|
||||
this.$summary_wrapper.removeClass('w-66').addClass('w-40');
|
||||
|
||||
// switch place holder with summary container
|
||||
this.$component.find('.no-summary-placeholder').addClass('d-none');
|
||||
this.$summary_wrapper.removeClass('d-none');
|
||||
}
|
||||
|
||||
switch_to_recent_invoice_summary() {
|
||||
// switch full width view with 60% view
|
||||
this.$component.removeClass('col-span-10').addClass('col-span-6');
|
||||
this.$summary_wrapper.removeClass('w-40').addClass('w-66');
|
||||
|
||||
// switch place holder with summary container
|
||||
this.$component.find('.no-summary-placeholder').addClass('d-none');
|
||||
this.$summary_wrapper.removeClass('d-none');
|
||||
}
|
||||
|
||||
get_condition_btn_map(after_submission) {
|
||||
if (after_submission)
|
||||
return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }];
|
||||
|
||||
return [
|
||||
{ condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] },
|
||||
{ condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']},
|
||||
{ condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt']}
|
||||
];
|
||||
}
|
||||
|
||||
load_summary_of(doc, after_submission=false) {
|
||||
this.$summary_wrapper.removeClass("d-none");
|
||||
|
||||
after_submission ?
|
||||
this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary();
|
||||
|
||||
this.doc = doc;
|
||||
|
||||
this.attach_basic_info(doc);
|
||||
|
||||
this.attach_items_info(doc);
|
||||
|
||||
this.attach_totals_info(doc);
|
||||
|
||||
this.attach_payments_info(doc);
|
||||
|
||||
const condition_btns_map = this.get_condition_btn_map(after_submission);
|
||||
|
||||
this.add_summary_btns(condition_btns_map);
|
||||
}
|
||||
|
||||
attach_basic_info(doc) {
|
||||
frappe.db.get_value('Customer', this.doc.customer, 'email_id').then(({ message }) => {
|
||||
this.customer_email = message.email_id || '';
|
||||
const upper_section_dom = this.get_upper_section_html(doc);
|
||||
this.$upper_section.html(upper_section_dom);
|
||||
});
|
||||
}
|
||||
|
||||
attach_items_info(doc) {
|
||||
this.$items_summary_container.html('');
|
||||
doc.items.forEach(item => {
|
||||
const item_dom = this.get_item_html(doc, item);
|
||||
this.$items_summary_container.append(item_dom);
|
||||
});
|
||||
}
|
||||
|
||||
attach_payments_info(doc) {
|
||||
this.$payment_summary_container.html('');
|
||||
doc.payments.forEach(p => {
|
||||
if (p.amount) {
|
||||
const payment_dom = this.get_payment_html(doc, p);
|
||||
this.$payment_summary_container.append(payment_dom);
|
||||
}
|
||||
});
|
||||
if (doc.redeem_loyalty_points && doc.loyalty_amount) {
|
||||
const payment_dom = this.get_payment_html(doc, {
|
||||
mode_of_payment: 'Loyalty Points',
|
||||
amount: doc.loyalty_amount,
|
||||
});
|
||||
this.$payment_summary_container.append(payment_dom);
|
||||
}
|
||||
}
|
||||
|
||||
attach_totals_info(doc) {
|
||||
this.$totals_summary_container.html('');
|
||||
|
||||
const discount_dom = this.get_discount_html(doc);
|
||||
const net_total_dom = this.get_net_total_html(doc);
|
||||
const taxes_dom = this.get_taxes_html(doc);
|
||||
const grand_total_dom = this.get_grand_total_html(doc);
|
||||
this.$totals_summary_container.append(discount_dom);
|
||||
this.$totals_summary_container.append(net_total_dom);
|
||||
this.$totals_summary_container.append(taxes_dom);
|
||||
this.$totals_summary_container.append(grand_total_dom);
|
||||
}
|
||||
|
||||
}
|
||||
503
erpnext/selling/page/point_of_sale/pos_payment.js
Normal file
503
erpnext/selling/page/point_of_sale/pos_payment.js
Normal file
@@ -0,0 +1,503 @@
|
||||
{% include "erpnext/selling/page/point_of_sale/pos_number_pad.js" %}
|
||||
|
||||
erpnext.PointOfSale.Payment = class {
|
||||
constructor({ events, wrapper }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
|
||||
this.init_component();
|
||||
}
|
||||
|
||||
init_component() {
|
||||
this.prepare_dom();
|
||||
this.initialize_numpad();
|
||||
this.bind_events();
|
||||
this.attach_shortcuts();
|
||||
|
||||
}
|
||||
|
||||
prepare_dom() {
|
||||
this.wrapper.append(
|
||||
`<section class="col-span-6 flex shadow rounded payment-section bg-white mx-h-70 h-100 d-none">
|
||||
<div class="flex flex-col p-16 pt-8 pb-8 w-full">
|
||||
<div class="text-grey mb-6 payment-section no-select pointer">
|
||||
PAYMENT METHOD<span class="octicon octicon-chevron-down collapse-indicator"></span>
|
||||
</div>
|
||||
<div class="payment-modes flex flex-wrap"></div>
|
||||
<div class="invoice-details-section"></div>
|
||||
<div class="flex mt-auto justify-center w-full">
|
||||
<div class="flex flex-col justify-center flex-1 ml-4">
|
||||
<div class="flex w-full">
|
||||
<div class="totals-remarks items-end justify-end flex flex-1">
|
||||
<div class="remarks text-md-0 text-grey mr-auto"></div>
|
||||
<div class="totals flex justify-end pt-4"></div>
|
||||
</div>
|
||||
<div class="number-pad w-40 mb-4 ml-8 d-none"></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center mt-4 submit-order h-16 w-full rounded bg-primary text-md text-white no-select pointer text-bold">
|
||||
Complete Order
|
||||
</div>
|
||||
<div class="order-time flex items-center justify-end mt-2 pt-2 pb-2 w-full text-md-0 text-grey no-select pointer d-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>`
|
||||
)
|
||||
this.$component = this.wrapper.find('.payment-section');
|
||||
this.$payment_modes = this.$component.find('.payment-modes');
|
||||
this.$totals_remarks = this.$component.find('.totals-remarks');
|
||||
this.$totals = this.$component.find('.totals');
|
||||
this.$remarks = this.$component.find('.remarks');
|
||||
this.$numpad = this.$component.find('.number-pad');
|
||||
this.$invoice_details_section = this.$component.find('.invoice-details-section');
|
||||
}
|
||||
|
||||
make_invoice_fields_control() {
|
||||
frappe.db.get_doc("POS Settings", undefined).then((doc) => {
|
||||
const fields = doc.invoice_fields;
|
||||
if (!fields.length) return;
|
||||
|
||||
this.$invoice_details_section.html(
|
||||
`<div class="text-grey pb-6 mt-2 pointer no-select">
|
||||
ADDITIONAL INFORMATION<span class="octicon octicon-chevron-down collapse-indicator"></span>
|
||||
</div>
|
||||
<div class="invoice-fields grid grid-cols-2 gap-4 mb-6 d-none"></div>`
|
||||
);
|
||||
this.$invoice_fields = this.$invoice_details_section.find('.invoice-fields');
|
||||
const frm = this.events.get_frm();
|
||||
|
||||
fields.forEach(df => {
|
||||
this.$invoice_fields.append(
|
||||
`<div class="invoice_detail_field ${df.fieldname}-field" data-fieldname="${df.fieldname}"></div>`
|
||||
);
|
||||
|
||||
this[`${df.fieldname}_field`] = frappe.ui.form.make_control({
|
||||
df: {
|
||||
...df,
|
||||
onchange: function() {
|
||||
frm.set_value(this.df.fieldname, this.value);
|
||||
}
|
||||
},
|
||||
parent: this.$invoice_fields.find(`.${df.fieldname}-field`),
|
||||
render_input: true,
|
||||
});
|
||||
this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
initialize_numpad() {
|
||||
const me = this;
|
||||
this.number_pad = new erpnext.PointOfSale.NumberPad({
|
||||
wrapper: this.$numpad,
|
||||
events: {
|
||||
numpad_event: function($btn) {
|
||||
me.on_numpad_clicked($btn);
|
||||
}
|
||||
},
|
||||
cols: 3,
|
||||
keys: [
|
||||
[ 1, 2, 3 ],
|
||||
[ 4, 5, 6 ],
|
||||
[ 7, 8, 9 ],
|
||||
[ '.', 0, 'Delete' ]
|
||||
],
|
||||
})
|
||||
|
||||
this.numpad_value = '';
|
||||
}
|
||||
|
||||
on_numpad_clicked($btn) {
|
||||
const me = this;
|
||||
const button_value = $btn.attr('data-button-value');
|
||||
|
||||
highlight_numpad_btn($btn);
|
||||
this.numpad_value = button_value === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + button_value;
|
||||
this.selected_mode.$input.get(0).focus();
|
||||
this.selected_mode.set_value(this.numpad_value);
|
||||
|
||||
function highlight_numpad_btn($btn) {
|
||||
$btn.addClass('shadow-inner bg-selected');
|
||||
setTimeout(() => {
|
||||
$btn.removeClass('shadow-inner bg-selected');
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
const me = this;
|
||||
|
||||
this.$payment_modes.on('click', '.mode-of-payment', function(e) {
|
||||
const mode_clicked = $(this);
|
||||
// if clicked element doesn't have .mode-of-payment class then return
|
||||
if (!$(e.target).is(mode_clicked)) return;
|
||||
|
||||
const mode = mode_clicked.attr('data-mode');
|
||||
|
||||
// hide all control fields and shortcuts
|
||||
$(`.mode-of-payment-control`).addClass('d-none');
|
||||
$(`.cash-shortcuts`).addClass('d-none');
|
||||
me.$payment_modes.find(`.pay-amount`).removeClass('d-none');
|
||||
me.$payment_modes.find(`.loyalty-amount-name`).addClass('d-none');
|
||||
|
||||
// remove highlight from all mode-of-payments
|
||||
$('.mode-of-payment').removeClass('border-primary');
|
||||
|
||||
if (mode_clicked.hasClass('border-primary')) {
|
||||
// clicked one is selected then unselect it
|
||||
mode_clicked.removeClass('border-primary');
|
||||
me.selected_mode = '';
|
||||
me.toggle_numpad(false);
|
||||
} else {
|
||||
// clicked one is not selected then select it
|
||||
mode_clicked.addClass('border-primary');
|
||||
mode_clicked.find('.mode-of-payment-control').removeClass('d-none');
|
||||
mode_clicked.find('.cash-shortcuts').removeClass('d-none');
|
||||
me.$payment_modes.find(`.${mode}-amount`).addClass('d-none');
|
||||
me.$payment_modes.find(`.${mode}-name`).removeClass('d-none');
|
||||
me.toggle_numpad(true);
|
||||
|
||||
me.selected_mode = me[`${mode}_control`];
|
||||
const doc = me.events.get_frm().doc;
|
||||
me.selected_mode?.$input?.get(0).focus();
|
||||
!me.selected_mode?.get_value() ? me.selected_mode?.set_value(doc.grand_total - doc.paid_amount) : '';
|
||||
}
|
||||
})
|
||||
|
||||
this.$payment_modes.on('click', '.shortcut', function(e) {
|
||||
const value = $(this).attr('data-value');
|
||||
me.selected_mode.set_value(value);
|
||||
})
|
||||
|
||||
// this.$totals_remarks.on('click', '.remarks', () => {
|
||||
// this.toggle_remarks_control();
|
||||
// })
|
||||
|
||||
this.$component.on('click', '.submit-order', () => {
|
||||
const doc = this.events.get_frm().doc;
|
||||
const paid_amount = doc.paid_amount;
|
||||
const items = doc.items;
|
||||
|
||||
if (paid_amount == 0 || !items.length) {
|
||||
const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order.")
|
||||
frappe.show_alert({ message, indicator: "orange" });
|
||||
frappe.utils.play_sound("error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.submit_invoice();
|
||||
})
|
||||
|
||||
frappe.ui.form.on('POS Invoice', 'paid_amount', (frm) => {
|
||||
this.update_totals_section(frm.doc);
|
||||
|
||||
// need to re calculate cash shortcuts after discount is applied
|
||||
const is_cash_shortcuts_invisible = this.$payment_modes.find('.cash-shortcuts').hasClass('d-none');
|
||||
this.attach_cash_shortcuts(frm.doc);
|
||||
!is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').removeClass('d-none');
|
||||
})
|
||||
|
||||
frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => {
|
||||
const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency);
|
||||
this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
|
||||
// for setting correct amount after loyalty points are redeemed
|
||||
const default_mop = locals[cdt][cdn];
|
||||
const mode = default_mop.mode_of_payment.replace(' ', '_').toLowerCase();
|
||||
if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) {
|
||||
this[`${mode}_control`].set_value(default_mop.amount);
|
||||
}
|
||||
});
|
||||
|
||||
this.$component.on('click', '.invoice-details-section', function(e) {
|
||||
if ($(e.target).closest('.invoice-fields').length) return;
|
||||
|
||||
me.$payment_modes.addClass('d-none');
|
||||
me.$invoice_fields.toggleClass("d-none");
|
||||
me.toggle_numpad(false);
|
||||
});
|
||||
this.$component.on('click', '.payment-section', () => {
|
||||
this.$invoice_fields.addClass("d-none");
|
||||
this.$payment_modes.toggleClass('d-none');
|
||||
this.toggle_numpad(true);
|
||||
})
|
||||
}
|
||||
|
||||
attach_shortcuts() {
|
||||
frappe.ui.keys.on("ctrl+enter", () => {
|
||||
const payment_is_visible = this.$component.is(":visible");
|
||||
const active_mode = this.$payment_modes.find(".border-primary");
|
||||
if (payment_is_visible && active_mode.length) {
|
||||
this.$component.find('.submit-order').click();
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.keys.on("tab", () => {
|
||||
const payment_is_visible = this.$component.is(":visible");
|
||||
const mode_of_payments = Array.from(this.$payment_modes.find(".mode-of-payment")).map(m => $(m).attr("data-mode"));
|
||||
let active_mode = this.$payment_modes.find(".border-primary");
|
||||
active_mode = active_mode.length ? active_mode.attr("data-mode") : undefined;
|
||||
|
||||
if (!active_mode) return;
|
||||
|
||||
const mode_index = mode_of_payments.indexOf(active_mode);
|
||||
const next_mode_index = (mode_index + 1) % mode_of_payments.length;
|
||||
const next_mode_to_be_clicked = this.$payment_modes.find(`.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]`);
|
||||
|
||||
if (payment_is_visible && mode_index != next_mode_index) {
|
||||
next_mode_to_be_clicked.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggle_numpad(show) {
|
||||
if (show) {
|
||||
this.$numpad.removeClass('d-none');
|
||||
this.$remarks.addClass('d-none');
|
||||
this.$totals_remarks.addClass('w-60 justify-center').removeClass('justify-end w-full');
|
||||
} else {
|
||||
this.$numpad.addClass('d-none');
|
||||
this.$remarks.removeClass('d-none');
|
||||
this.$totals_remarks.removeClass('w-60 justify-center').addClass('justify-end w-full');
|
||||
}
|
||||
}
|
||||
|
||||
render_payment_section() {
|
||||
this.render_payment_mode_dom();
|
||||
this.make_invoice_fields_control();
|
||||
this.update_totals_section();
|
||||
}
|
||||
|
||||
edit_cart() {
|
||||
this.events.toggle_other_sections(false);
|
||||
this.toggle_component(false);
|
||||
}
|
||||
|
||||
checkout() {
|
||||
this.events.toggle_other_sections(true);
|
||||
this.toggle_component(true);
|
||||
|
||||
this.render_payment_section();
|
||||
}
|
||||
|
||||
toggle_remarks_control() {
|
||||
if (this.$remarks.find('.frappe-control').length) {
|
||||
this.$remarks.html('+ Add Remark');
|
||||
} else {
|
||||
this.$remarks.html('');
|
||||
this[`remark_control`] = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Remark'),
|
||||
fieldtype: 'Data',
|
||||
onchange: function() {}
|
||||
},
|
||||
parent: this.$totals_remarks.find(`.remarks`),
|
||||
render_input: true,
|
||||
});
|
||||
this[`remark_control`].set_value('');
|
||||
}
|
||||
}
|
||||
|
||||
render_payment_mode_dom() {
|
||||
const doc = this.events.get_frm().doc;
|
||||
const payments = doc.payments;
|
||||
const currency = doc.currency;
|
||||
|
||||
this.$payment_modes.html(
|
||||
`${
|
||||
payments.map((p, i) => {
|
||||
const mode = p.mode_of_payment.replace(' ', '_').toLowerCase();
|
||||
const payment_type = p.type;
|
||||
const margin = i % 2 === 0 ? 'pr-2' : 'pl-2';
|
||||
const amount = p.amount > 0 ? format_currency(p.amount, currency) : '';
|
||||
|
||||
return (
|
||||
`<div class="w-half ${margin} bg-white">
|
||||
<div class="mode-of-payment rounded border border-grey text-grey text-md
|
||||
mb-4 p-8 pt-4 pb-4 no-select pointer" data-mode="${mode}" data-payment-type="${payment_type}">
|
||||
${p.mode_of_payment}
|
||||
<div class="${mode}-amount pay-amount inline float-right text-bold">${amount}</div>
|
||||
<div class="${mode} mode-of-payment-control mt-4 flex flex-1 items-center d-none"></div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
}).join('')
|
||||
}`
|
||||
)
|
||||
|
||||
payments.forEach(p => {
|
||||
const mode = p.mode_of_payment.replace(' ', '_').toLowerCase();
|
||||
const me = this;
|
||||
this[`${mode}_control`] = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __(`${p.mode_of_payment}`),
|
||||
fieldtype: 'Currency',
|
||||
placeholder: __(`Enter ${p.mode_of_payment} amount.`),
|
||||
onchange: function() {
|
||||
if (this.value || this.value == 0) {
|
||||
frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value))
|
||||
.then(() => me.update_totals_section());
|
||||
|
||||
const formatted_currency = format_currency(this.value, currency);
|
||||
me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency);
|
||||
}
|
||||
}
|
||||
},
|
||||
parent: this.$payment_modes.find(`.${mode}.mode-of-payment-control`),
|
||||
render_input: true,
|
||||
});
|
||||
this[`${mode}_control`].toggle_label(false);
|
||||
this[`${mode}_control`].set_value(p.amount);
|
||||
|
||||
if (p.default) {
|
||||
setTimeout(() => {
|
||||
this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
|
||||
}, 500);
|
||||
}
|
||||
})
|
||||
|
||||
this.render_loyalty_points_payment_mode();
|
||||
|
||||
this.attach_cash_shortcuts(doc);
|
||||
}
|
||||
|
||||
attach_cash_shortcuts(doc) {
|
||||
const grand_total = doc.grand_total;
|
||||
const currency = doc.currency;
|
||||
|
||||
const shortcuts = this.get_cash_shortcuts(flt(grand_total));
|
||||
|
||||
this.$payment_modes.find('.cash-shortcuts').remove();
|
||||
this.$payment_modes.find('[data-payment-type="Cash"]').find('.mode-of-payment-control').after(
|
||||
`<div class="cash-shortcuts grid grid-cols-3 gap-2 flex-1 text-center text-md-0 mb-2 d-none">
|
||||
${
|
||||
shortcuts.map(s => {
|
||||
return `<div class="shortcut rounded bg-light-grey text-dark-grey pt-2 pb-2 no-select pointer" data-value="${s}">
|
||||
${format_currency(s, currency)}
|
||||
</div>`
|
||||
}).join('')
|
||||
}
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
|
||||
get_cash_shortcuts(grand_total) {
|
||||
let steps = [1, 5, 10];
|
||||
const digits = String(Math.round(grand_total)).length;
|
||||
|
||||
steps = steps.map(x => x * (10 ** (digits - 2)));
|
||||
|
||||
const get_nearest = (amount, x) => {
|
||||
let nearest_x = Math.ceil((amount / x)) * x;
|
||||
return nearest_x === amount ? nearest_x + x : nearest_x;
|
||||
}
|
||||
|
||||
return steps.reduce((finalArr, x) => {
|
||||
let nearest_x = get_nearest(grand_total, x);
|
||||
nearest_x = finalArr.indexOf(nearest_x) != -1 ? nearest_x + x : nearest_x;
|
||||
return [...finalArr, nearest_x];
|
||||
}, []);
|
||||
}
|
||||
|
||||
render_loyalty_points_payment_mode() {
|
||||
const me = this;
|
||||
const doc = this.events.get_frm().doc;
|
||||
const { loyalty_program, loyalty_points, conversion_factor } = this.events.get_customer_details();
|
||||
|
||||
this.$payment_modes.find(`.mode-of-payment[data-mode="loyalty-amount"]`).parent().remove();
|
||||
|
||||
if (!loyalty_program) return;
|
||||
|
||||
let description, read_only, max_redeemable_amount;
|
||||
if (!loyalty_points) {
|
||||
description = __(`You don't have enough points to redeem.`);
|
||||
read_only = true;
|
||||
} else {
|
||||
max_redeemable_amount = flt(flt(loyalty_points) * flt(conversion_factor), precision("loyalty_amount", doc))
|
||||
description = __(`You can redeem upto ${format_currency(max_redeemable_amount)}.`);
|
||||
read_only = false;
|
||||
}
|
||||
|
||||
const margin = this.$payment_modes.children().length % 2 === 0 ? 'pr-2' : 'pl-2';
|
||||
const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : '';
|
||||
this.$payment_modes.append(
|
||||
`<div class="w-half ${margin} bg-white">
|
||||
<div class="mode-of-payment rounded border border-grey text-grey text-md
|
||||
mb-4 p-8 pt-4 pb-4 no-select pointer" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
|
||||
Redeem Loyalty Points
|
||||
<div class="loyalty-amount-amount pay-amount inline float-right text-bold">${amount}</div>
|
||||
<div class="loyalty-amount-name inline float-right text-bold text-md-0 d-none">${loyalty_program}</div>
|
||||
<div class="loyalty-amount mode-of-payment-control mt-4 flex flex-1 items-center d-none"></div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
|
||||
this['loyalty-amount_control'] = frappe.ui.form.make_control({
|
||||
df: {
|
||||
label: __('Redeem Loyalty Points'),
|
||||
fieldtype: 'Currency',
|
||||
placeholder: __(`Enter amount to be redeemed.`),
|
||||
options: 'company:currency',
|
||||
read_only,
|
||||
onchange: async function() {
|
||||
if (!loyalty_points) return;
|
||||
|
||||
if (this.value > max_redeemable_amount) {
|
||||
frappe.show_alert({
|
||||
message: __(`You cannot redeem more than ${format_currency(max_redeemable_amount)}.`),
|
||||
indicator: "red"
|
||||
});
|
||||
frappe.utils.play_sound("submit");
|
||||
me['loyalty-amount_control'].set_value(0);
|
||||
return;
|
||||
}
|
||||
const redeem_loyalty_points = this.value > 0 ? 1 : 0;
|
||||
await frappe.model.set_value(doc.doctype, doc.name, 'redeem_loyalty_points', redeem_loyalty_points);
|
||||
frappe.model.set_value(doc.doctype, doc.name, 'loyalty_points', parseInt(this.value / conversion_factor));
|
||||
},
|
||||
description
|
||||
},
|
||||
parent: this.$payment_modes.find(`.loyalty-amount.mode-of-payment-control`),
|
||||
render_input: true,
|
||||
});
|
||||
this['loyalty-amount_control'].toggle_label(false);
|
||||
|
||||
// this.render_add_payment_method_dom();
|
||||
}
|
||||
|
||||
render_add_payment_method_dom() {
|
||||
const docstatus = this.events.get_frm().doc.docstatus;
|
||||
if (docstatus === 0)
|
||||
this.$payment_modes.append(
|
||||
`<div class="w-full pr-2">
|
||||
<div class="add-mode-of-payment w-half text-grey mb-4 no-select pointer">+ Add Payment Method</div>
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
|
||||
update_totals_section(doc) {
|
||||
if (!doc) doc = this.events.get_frm().doc;
|
||||
const paid_amount = doc.paid_amount;
|
||||
const remaining = doc.grand_total - doc.paid_amount;
|
||||
const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
|
||||
const currency = doc.currency
|
||||
const label = change ? __('Change') : __('To Be Paid');
|
||||
|
||||
this.$totals.html(
|
||||
`<div>
|
||||
<div class="pr-8 border-r-grey">Paid Amount</div>
|
||||
<div class="pr-8 border-r-grey text-bold text-2xl">${format_currency(paid_amount, currency)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="pl-8">${label}</div>
|
||||
<div class="pl-8 text-green-400 text-bold text-2xl">${format_currency(change || remaining, currency)}</div>
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
|
||||
toggle_component(show) {
|
||||
show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
QUnit.test("test:Point of Sales", function(assert) {
|
||||
assert.expect(1);
|
||||
let done = assert.async();
|
||||
|
||||
frappe.run_serially([
|
||||
() => frappe.set_route('point-of-sale'),
|
||||
() => frappe.timeout(3),
|
||||
() => frappe.set_control('customer', 'Test Customer 1'),
|
||||
() => frappe.timeout(0.2),
|
||||
() => cur_frm.set_value('customer', 'Test Customer 1'),
|
||||
() => frappe.timeout(2),
|
||||
() => frappe.click_link('Test Product 2'),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.cart-items [data-item-code="Test Product 2"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.number-pad [data-value="Rate"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.number-pad [data-value="2"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.number-pad [data-value="5"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.number-pad [data-value="0"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.number-pad [data-value="Pay"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.frappe-control [data-value="4"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.frappe-control [data-value="5"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_element(`.frappe-control [data-value="0"]`),
|
||||
() => frappe.timeout(0.2),
|
||||
() => frappe.click_button('Submit'),
|
||||
() => frappe.click_button('Yes'),
|
||||
() => frappe.timeout(3),
|
||||
() => assert.ok(cur_frm.doc.docstatus==1, "Sales invoice created successfully"),
|
||||
() => done()
|
||||
]);
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
22
erpnext/selling/print_format/pos_invoice/pos_invoice.json
Normal file
22
erpnext/selling/print_format/pos_invoice/pos_invoice.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"align_labels_right": 0,
|
||||
"creation": "2011-12-21 11:08:55",
|
||||
"custom_format": 1,
|
||||
"disabled": 0,
|
||||
"doc_type": "POS Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Invoice\") }}<br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
|
||||
"idx": 1,
|
||||
"line_breaks": 0,
|
||||
"modified": "2020-04-29 16:45:58.942375",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "POS Invoice",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"align_labels_right": 0,
|
||||
"creation": "2020-05-14 17:02:44.207166",
|
||||
"custom_format": 1,
|
||||
"default_print_language": "en",
|
||||
"disabled": 0,
|
||||
"doc_type": "POS Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Default",
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Return Invoice\") }}<br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Original Invoice\") }}:</b> {{ doc.return_against }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc)}}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\")}}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"modified": "2020-05-14 17:13:29.354015",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Return POS Invoice",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
||||
@@ -191,7 +191,7 @@ def get_conditions(filters):
|
||||
conditions += "AND so_item.item_code = '%s'" %frappe.db.escape(filters.item_code)
|
||||
|
||||
if filters.get("customer"):
|
||||
conditions += "AND so.customer = '%s'" %frappe.db.escape(filters.customer)
|
||||
conditions += "AND so.customer = %s" %frappe.db.escape(filters.customer)
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ def prepare_data(data, filters):
|
||||
# prepare data for report view
|
||||
row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"])
|
||||
|
||||
row["delay"] = 0 if row["delay"] < 0 else row["delay"]
|
||||
row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"]
|
||||
if filters.get("group_by_so"):
|
||||
so_name = row["sales_order"]
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
||||
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
|
||||
|
||||
// check if child doctype is Sales Order Item/Qutation Item and calculate the rate
|
||||
if(in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item"]), cdt)
|
||||
if(in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item"]), cdt)
|
||||
this.apply_pricing_rule_on_item(item);
|
||||
else
|
||||
item.rate = flt(item.price_list_rate * (1 - item.discount_percentage / 100.0),
|
||||
@@ -312,6 +312,11 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
||||
batch_no: function(doc, cdt, cdn) {
|
||||
var me = this;
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
|
||||
if (item.serial_no) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.serial_no = null;
|
||||
var has_serial_no;
|
||||
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_serial_no', (r) => {
|
||||
@@ -489,13 +494,18 @@ frappe.ui.form.on(cur_frm.doctype, {
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Set as Lost"),
|
||||
fields: [
|
||||
{"fieldtype": "Table MultiSelect",
|
||||
"label": __("Lost Reasons"),
|
||||
"fieldname": "lost_reason",
|
||||
"options": "Lost Reason Detail",
|
||||
"reqd": 1},
|
||||
|
||||
{"fieldtype": "Text", "label": __("Detailed Reason"), "fieldname": "detailed_reason"},
|
||||
{
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": __("Lost Reasons"),
|
||||
"fieldname": "lost_reason",
|
||||
"options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail',
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldtype": "Text",
|
||||
"label": __("Detailed Reason"),
|
||||
"fieldname": "detailed_reason"
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
var values = dialog.get_values();
|
||||
|
||||
46
erpnext/selling/selling_dashboard/selling/selling.json
Normal file
46
erpnext/selling/selling_dashboard/selling/selling.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"cards": [
|
||||
{
|
||||
"card": "Annual Sales"
|
||||
},
|
||||
{
|
||||
"card": "Sales Orders to Deliver"
|
||||
},
|
||||
{
|
||||
"card": "Sales Orders to Bill"
|
||||
},
|
||||
{
|
||||
"card": "Active Customers"
|
||||
}
|
||||
],
|
||||
"charts": [
|
||||
{
|
||||
"chart": "Sales Order Trends",
|
||||
"width": "Full"
|
||||
},
|
||||
{
|
||||
"chart": "Top Customers",
|
||||
"width": "Half"
|
||||
},
|
||||
{
|
||||
"chart": "Sales Order Analysis",
|
||||
"width": "Half"
|
||||
},
|
||||
{
|
||||
"chart": "Item-wise Annual Sales",
|
||||
"width": "Full"
|
||||
}
|
||||
],
|
||||
"creation": "2020-07-20 20:17:16.688162",
|
||||
"dashboard_name": "Selling",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard",
|
||||
"idx": 0,
|
||||
"is_default": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 15:31:22.299903",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling",
|
||||
"owner": "Administrator"
|
||||
}
|
||||
Reference in New Issue
Block a user