Merge branch 'develop' into subscription_fix_new
@ -30,6 +30,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"read_only_depends_on": "eval:!doc.__islocal",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -48,7 +49,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-22 20:34:39.805728",
|
||||
"modified": "2021-02-08 16:37:53.936656",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension",
|
||||
|
@ -29,6 +29,16 @@ class AccountingDimension(Document):
|
||||
if exists and self.is_new():
|
||||
frappe.throw("Document Type already used as a dimension")
|
||||
|
||||
if not self.is_new():
|
||||
self.validate_document_type_change()
|
||||
|
||||
def validate_document_type_change(self):
|
||||
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
|
||||
if doctype_before_save != self.document_type:
|
||||
message = _("Cannot change Reference Document Type.")
|
||||
message += _("Please create a new Accounting Dimension if required.")
|
||||
frappe.throw(message)
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
|
@ -108,7 +108,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-16 15:27:23.659285",
|
||||
"modified": "2021-02-03 12:04:58.678402",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension Filter",
|
||||
@ -125,6 +125,30 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
@ -88,19 +88,19 @@ class PaymentReconciliation(Document):
|
||||
voucher_type = ('Sales Invoice'
|
||||
if self.party_type == 'Customer' else "Purchase Invoice")
|
||||
|
||||
return frappe.db.sql(""" SELECT `tab{doc}`.name as reference_name, %(voucher_type)s as reference_type,
|
||||
(sum(`tabGL Entry`.{dr_or_cr}) - sum(`tabGL Entry`.{reconciled_dr_or_cr})) as amount,
|
||||
return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
|
||||
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount,
|
||||
account_currency as currency
|
||||
FROM `tab{doc}`, `tabGL Entry`
|
||||
FROM `tab{doc}` doc, `tabGL Entry` gl
|
||||
WHERE
|
||||
(`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no)
|
||||
and `tab{doc}`.{party_type_field} = %(party)s
|
||||
and `tab{doc}`.is_return = 1 and `tab{doc}`.return_against IS NULL
|
||||
and `tabGL Entry`.against_voucher_type = %(voucher_type)s
|
||||
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
|
||||
and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s
|
||||
and `tabGL Entry`.is_cancelled = 0
|
||||
GROUP BY `tab{doc}`.name
|
||||
(doc.name = gl.against_voucher or doc.name = gl.voucher_no)
|
||||
and doc.{party_type_field} = %(party)s
|
||||
and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
|
||||
and gl.against_voucher_type = %(voucher_type)s
|
||||
and doc.docstatus = 1 and gl.party = %(party)s
|
||||
and gl.party_type = %(party_type)s and gl.account = %(account)s
|
||||
and gl.is_cancelled = 0
|
||||
GROUP BY doc.name
|
||||
Having
|
||||
amount > 0
|
||||
""".format(
|
||||
@ -113,7 +113,7 @@ class PaymentReconciliation(Document):
|
||||
'party_type': self.party_type,
|
||||
'voucher_type': voucher_type,
|
||||
'account': self.receivable_payable_account
|
||||
}, as_dict=1)
|
||||
}, as_dict=1, debug=1)
|
||||
|
||||
def add_payment_entries(self, entries):
|
||||
self.set('payments', [])
|
||||
|
@ -52,8 +52,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
|
||||
row = {
|
||||
'item_code': d.item_code,
|
||||
'item_name': item_record.item_name,
|
||||
'item_group': item_record.item_group,
|
||||
'item_name': item_record.item_name if item_record else d.item_name,
|
||||
'item_group': item_record.item_group if item_record else d.item_group,
|
||||
'description': d.description,
|
||||
'invoice': d.parent,
|
||||
'posting_date': d.posting_date,
|
||||
@ -383,6 +383,7 @@ def get_items(filters, additional_query_columns):
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||
`tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
|
||||
`tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
|
||||
|
@ -352,7 +352,7 @@ def get_lead_with_phone_number(number):
|
||||
leads = frappe.get_all('Lead', or_filters={
|
||||
'phone': ['like', '%{}'.format(number)],
|
||||
'mobile_no': ['like', '%{}'.format(number)]
|
||||
}, limit=1)
|
||||
}, limit=1, order_by="creation DESC")
|
||||
|
||||
lead = leads[0].name if leads else None
|
||||
|
||||
@ -361,4 +361,4 @@ def get_lead_with_phone_number(number):
|
||||
def daily_open_lead():
|
||||
leads = frappe.get_all("Lead", filters = [["contact_date", "Between", [nowdate(), nowdate()]]])
|
||||
for lead in leads:
|
||||
frappe.db.set_value("Lead", lead.name, "status", "Open")
|
||||
frappe.db.set_value("Lead", lead.name, "status", "Open")
|
||||
|
@ -139,12 +139,13 @@ def create_inpatient(patient):
|
||||
inpatient_record.phone = patient_obj.phone
|
||||
inpatient_record.inpatient = "Scheduled"
|
||||
inpatient_record.scheduled_date = today()
|
||||
inpatient_record.company = "_Test Company"
|
||||
return inpatient_record
|
||||
|
||||
|
||||
def get_healthcare_service_unit(unit_name=None):
|
||||
if not unit_name:
|
||||
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
|
||||
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1, "company": "_Test Company"})
|
||||
else:
|
||||
service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name})
|
||||
|
||||
|
@ -119,7 +119,7 @@ def create_records(patient):
|
||||
ip_record.expected_length_of_stay = 0
|
||||
ip_record.save()
|
||||
ip_record.reload()
|
||||
service_unit = get_healthcare_service_unit()
|
||||
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
|
||||
admit_patient(ip_record, service_unit, now_datetime())
|
||||
|
||||
ipmo = create_ipmo(patient)
|
||||
|
@ -78,7 +78,7 @@ website_generators = ["Item Group", "Item", "BOM", "Sales Partner",
|
||||
"Job Opening", "Student Admission"]
|
||||
|
||||
website_context = {
|
||||
"favicon": "/assets/erpnext/images/favicon.png",
|
||||
"favicon": "/assets/erpnext/images/erpnext-favicon.svg",
|
||||
"splash_image": "/assets/erpnext/images/erpnext-logo.svg"
|
||||
}
|
||||
|
||||
|
@ -16,11 +16,13 @@ class TestEmployee(unittest.TestCase):
|
||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
|
||||
employee.company_email = "test@example.com"
|
||||
employee.company = "_Test Company"
|
||||
employee.save()
|
||||
|
||||
from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders
|
||||
|
||||
self.assertTrue(employee.name in [e.name for e in get_employees_who_are_born_today()])
|
||||
employees_born_today = get_employees_who_are_born_today()
|
||||
self.assertTrue(employees_born_today.get("_Test Company"))
|
||||
|
||||
frappe.db.sql("delete from `tabEmail Queue`")
|
||||
|
||||
|
@ -36,6 +36,8 @@ def execute(filters=None):
|
||||
conditions, filters = get_conditions(filters)
|
||||
columns, days = get_columns(filters)
|
||||
att_map = get_attendance_list(conditions, filters)
|
||||
if not att_map:
|
||||
return columns, [], None, None
|
||||
|
||||
if filters.group_by:
|
||||
emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company)
|
||||
@ -65,10 +67,14 @@ def execute(filters=None):
|
||||
if filters.group_by:
|
||||
emp_att_map = {}
|
||||
for parameter in group_by_parameters:
|
||||
data.append([ "<b>"+ parameter + "</b>"])
|
||||
record, aaa = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
|
||||
emp_att_map.update(aaa)
|
||||
data += record
|
||||
emp_map_set = set([key for key in emp_map[parameter].keys()])
|
||||
att_map_set = set([key for key in att_map.keys()])
|
||||
if (att_map_set & emp_map_set):
|
||||
parameter_row = ["<b>"+ parameter + "</b>"] + ['' for day in range(filters["total_days_in_month"] + 2)]
|
||||
data.append(parameter_row)
|
||||
record, emp_att_data = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
|
||||
emp_att_map.update(emp_att_data)
|
||||
data += record
|
||||
else:
|
||||
record, emp_att_map = add_data(emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
|
||||
data += record
|
||||
@ -237,6 +243,9 @@ def get_attendance_list(conditions, filters):
|
||||
status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" %
|
||||
conditions, filters, as_dict=1)
|
||||
|
||||
if not attendance_list:
|
||||
msgprint(_("No attendance record found"), alert=True, indicator="orange")
|
||||
|
||||
att_map = {}
|
||||
for d in attendance_list:
|
||||
att_map.setdefault(d.employee, frappe._dict()).setdefault(d.day_of_month, "")
|
||||
|
@ -386,7 +386,8 @@ def get_items(filters=None, search=None):
|
||||
r.description = r.web_long_description or r.description
|
||||
r.image = r.website_image or r.image
|
||||
product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info')
|
||||
r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
|
||||
if product_info:
|
||||
r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
|
||||
|
||||
return results
|
||||
|
||||
|
@ -288,7 +288,7 @@ def make_sales_invoice(source_name, item_code=None, customer=None):
|
||||
def make_salary_slip(source_name, target_doc=None):
|
||||
target = frappe.new_doc("Salary Slip")
|
||||
set_missing_values(source_name, target)
|
||||
target.run_method("get_emp_and_leave_details")
|
||||
target.run_method("get_emp_and_working_day_details")
|
||||
|
||||
return target
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"css/erpnext.css": [
|
||||
"public/less/erpnext.less",
|
||||
"public/less/hub.less",
|
||||
"public/less/call_popup.less",
|
||||
"public/scss/call_popup.scss",
|
||||
"public/scss/point-of-sale.scss"
|
||||
],
|
||||
"css/marketplace.css": [
|
||||
|
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="88px" height="88px" viewBox="0 0 88 88" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>erpnext-logo</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="erpnext-logo" transform="translate(-2.000000, -2.000000)" fill-rule="nonzero">
|
||||
<g id="g1422-7-2" transform="translate(0.025630, 0.428785)" fill="#5E64FF">
|
||||
<g id="g1418-4-6" transform="translate(0.268998, 0.867736)">
|
||||
<g id="g1416-4-9" transform="translate(0.749997, 0.000000)">
|
||||
<path d="M14.1845844,0.703479866 L75.0387175,0.703479866 C82.3677094,0.703479866 88.2679029,6.60367875 88.2679029,13.9326374 L88.2679029,74.7868158 C88.2679029,82.1157744 82.3677094,88.0159833 75.0387175,88.0159833 L14.1845844,88.0159833 C6.85569246,88.0159833 0.955398949,82.1157744 0.955398949,74.7868158 L0.955398949,13.9326374 C0.955398949,6.60367875 6.85569246,0.703479866 14.1845844,0.703479866 L14.1845844,0.703479866 Z" id="path1414-3-4"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="g1444-6-7" transform="translate(27.708247, 23.320960)" fill="#FFFFFF">
|
||||
<path d="M4.06942472,0.507006595 C3.79457554,0.507006595 3.52673783,0.534925429 3.26792241,0.587619847 C3.00908052,0.640314265 2.75926093,0.717948309 2.52171801,0.818098395 C2.40292009,0.868173438 2.28745592,0.924056085 2.17495509,0.985013441 C1.94997987,1.10692286 1.73828674,1.24983755 1.54244215,1.41134187 C0.661062132,2.13811791 0.100674618,3.23899362 0.100674618,4.4757567 L0.100674618,4.71760174 L0.100674618,39.9531653 L0.100674618,40.1945182 C0.100674618,42.3932057 1.87073716,44.1632683 4.06942472,44.1632683 L31.8263867,44.1632683 C34.0250742,44.1632683 35.7951368,42.3932057 35.7951368,40.1945182 L35.7951368,39.9531653 C35.7951368,37.7544777 34.0250742,35.9844152 31.8263867,35.9844152 L8.28000399,35.9844152 L8.28000399,26.0992376 L25.7874571,26.0992376 C27.9861447,26.0992376 29.7562072,24.3291751 29.7562072,22.1304875 L29.7562072,21.8891611 C29.7562072,19.6904735 27.9861447,17.920411 25.7874571,17.920411 L8.28000399,17.920411 L8.28000399,8.68635184 L31.8263867,8.68635184 C34.0250742,8.68635184 35.7951368,6.9162893 35.7951368,4.71760174 L35.7951368,4.4757567 C35.7951368,2.27706914 34.0250742,0.507006595 31.8263867,0.507006595 L4.06942472,0.507006595 Z" id="rect1436-8-4"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.6 KiB |
@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="165px" height="88px" viewBox="0 0 165 88" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>version-12</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="version-12" transform="translate(-2.000000, -2.000000)">
|
||||
<g id="erp-icon" fill-rule="nonzero">
|
||||
<g id="g1422-7-2" transform="translate(0.025630, 0.428785)" fill="#5E64FF">
|
||||
<g id="g1418-4-6" transform="translate(0.268998, 0.867736)">
|
||||
<g id="g1416-4-9" transform="translate(0.749997, 0.000000)">
|
||||
<path d="M14.1845844,0.703479866 L75.0387175,0.703479866 C82.3677094,0.703479866 88.2679029,6.60367875 88.2679029,13.9326374 L88.2679029,74.7868158 C88.2679029,82.1157744 82.3677094,88.0159833 75.0387175,88.0159833 L14.1845844,88.0159833 C6.85569246,88.0159833 0.955398949,82.1157744 0.955398949,74.7868158 L0.955398949,13.9326374 C0.955398949,6.60367875 6.85569246,0.703479866 14.1845844,0.703479866 L14.1845844,0.703479866 Z" id="path1414-3-4"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="g1444-6-7" transform="translate(27.708247, 23.320960)" fill="#FFFFFF">
|
||||
<path d="M4.06942472,0.507006595 C3.79457554,0.507006595 3.52673783,0.534925429 3.26792241,0.587619847 C3.00908052,0.640314265 2.75926093,0.717948309 2.52171801,0.818098395 C2.40292009,0.868173438 2.28745592,0.924056085 2.17495509,0.985013441 C1.94997987,1.10692286 1.73828674,1.24983755 1.54244215,1.41134187 C0.661062132,2.13811791 0.100674618,3.23899362 0.100674618,4.4757567 L0.100674618,4.71760174 L0.100674618,39.9531653 L0.100674618,40.1945182 C0.100674618,42.3932057 1.87073716,44.1632683 4.06942472,44.1632683 L31.8263867,44.1632683 C34.0250742,44.1632683 35.7951368,42.3932057 35.7951368,40.1945182 L35.7951368,39.9531653 C35.7951368,37.7544777 34.0250742,35.9844152 31.8263867,35.9844152 L8.28000399,35.9844152 L8.28000399,26.0992376 L25.7874571,26.0992376 C27.9861447,26.0992376 29.7562072,24.3291751 29.7562072,22.1304875 L29.7562072,21.8891611 C29.7562072,19.6904735 27.9861447,17.920411 25.7874571,17.920411 L8.28000399,17.920411 L8.28000399,8.68635184 L31.8263867,8.68635184 C34.0250742,8.68635184 35.7951368,6.9162893 35.7951368,4.71760174 L35.7951368,4.4757567 C35.7951368,2.27706914 34.0250742,0.507006595 31.8263867,0.507006595 L4.06942472,0.507006595 Z" id="rect1436-8-4"></path>
|
||||
</g>
|
||||
</g>
|
||||
<text id="12" font-family="SourceSansPro-Regular, Source Sans Pro" font-size="72" font-weight="normal" letter-spacing="-0.386831313" fill="#D1D8DD">
|
||||
<tspan x="99" y="71">12</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.9 KiB |
5
erpnext/public/images/erpnext-favicon.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 12C0 5.37258 5.37258 0 12 0H88C94.6274 0 100 5.37258 100 12V88C100 94.6274 94.6274 100 88 100H12C5.37258 100 0 94.6274 0 88V12Z" fill="#0089FF"/>
|
||||
<path d="M65.7097 32.9462H67.3871V24H33V32.9462H43.9032H65.7097Z" fill="white"/>
|
||||
<path d="M43.9032 66.2151V53.914H65.7097V44.9677H43.9032H33V75.1613H67.6667V66.2151H43.9032Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 454 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 11 KiB |
@ -1,4 +0,0 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.45455 0H30.5454C33.5673 0 36 2.43272 36 5.45454V30.5455C36 33.5673 33.5673 36 30.5454 36H5.45455C2.43276 36 0 33.5673 0 30.5455V5.45454C0 2.43272 2.43276 0 5.45455 0Z" fill="#2996F1"/>
|
||||
<path d="M12.277 8.99994C12.1637 8.99994 12.0532 9.01145 11.9465 9.03318C11.8398 9.0549 11.7368 9.08691 11.6389 9.12821C11.5899 9.14885 11.5423 9.17189 11.4959 9.19703C11.4031 9.24729 11.3158 9.30622 11.2351 9.37281C10.8717 9.67247 10.6406 10.1264 10.6406 10.6363V10.736V25.2641V25.3636C10.6406 26.2701 11.3704 26.9999 12.277 26.9999H23.7215C24.6281 26.9999 25.3579 26.2701 25.3579 25.3636V25.2641C25.3579 24.3575 24.6281 23.6277 23.7215 23.6277H14.0131V19.5519H21.2316C22.1381 19.5519 22.868 18.8221 22.868 17.9156V17.8161C22.868 16.9095 22.1381 16.1797 21.2316 16.1797H14.0131V12.3724H23.7215C24.6281 12.3724 25.3579 11.6426 25.3579 10.736V10.6363C25.3579 9.72976 24.6281 8.99994 23.7215 8.99994H12.277Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1023 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 10 KiB |
@ -7,10 +7,103 @@ class CallPopup {
|
||||
}
|
||||
|
||||
make() {
|
||||
frappe.utils.play_sound('incoming-call');
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
'static': true,
|
||||
'minimizable': true,
|
||||
'fields': [{
|
||||
'minimizable': true
|
||||
});
|
||||
this.dialog.get_close_btn().show();
|
||||
this.setup_dialog();
|
||||
this.set_call_status();
|
||||
frappe.utils.bind_actions_with_object(this.dialog.$body, this);
|
||||
this.dialog.$wrapper.addClass('call-popup');
|
||||
this.dialog.get_close_btn().unbind('click').click(this.close_modal.bind(this));
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
setup_dialog() {
|
||||
this.setup_call_details();
|
||||
this.dialog.$body.empty().append(this.caller_info);
|
||||
}
|
||||
|
||||
set_indicator(color, blink=false) {
|
||||
let classes = `indicator ${color} ${blink ? 'blink': ''}`;
|
||||
this.dialog.header.find('.indicator').attr('class', classes);
|
||||
}
|
||||
|
||||
set_call_status(call_status) {
|
||||
let title = '';
|
||||
call_status = call_status || this.call_log.status;
|
||||
if (['Ringing'].includes(call_status) || !call_status) {
|
||||
title = __('Incoming call from {0}', [this.get_caller_name() || this.caller_number]);
|
||||
this.set_indicator('blue', true);
|
||||
} else if (call_status === 'In Progress') {
|
||||
title = __('Call Connected');
|
||||
this.set_indicator('green');
|
||||
} else if (['No Answer', 'Missed'].includes(call_status)) {
|
||||
this.set_indicator('yellow');
|
||||
title = __('Call Missed');
|
||||
} else if (['Completed', 'Busy', 'Failed'].includes(call_status)) {
|
||||
this.set_indicator('red');
|
||||
title = __('Call Ended');
|
||||
} else {
|
||||
this.set_indicator('blue');
|
||||
title = call_status;
|
||||
}
|
||||
this.dialog.set_title(title);
|
||||
}
|
||||
|
||||
update_call_log(call_log, missed) {
|
||||
this.call_log = call_log;
|
||||
this.set_call_status(missed ? 'Missed': null);
|
||||
}
|
||||
|
||||
close_modal() {
|
||||
this.dialog.hide();
|
||||
delete erpnext.call_popup;
|
||||
}
|
||||
|
||||
call_ended(call_log, missed) {
|
||||
frappe.utils.play_sound('call-disconnect');
|
||||
this.update_call_log(call_log, missed);
|
||||
setTimeout(() => {
|
||||
if (!this.dialog.get_value('call_summary')) {
|
||||
this.close_modal();
|
||||
}
|
||||
}, 60000);
|
||||
this.clear_listeners();
|
||||
}
|
||||
|
||||
get_caller_name() {
|
||||
const contact_link = this.get_contact_link();
|
||||
return contact_link.link_title || contact_link.link_name;
|
||||
}
|
||||
|
||||
get_contact_link() {
|
||||
let log = this.call_log;
|
||||
let contact_link = log.links.find(d => d.link_doctype === 'Contact');
|
||||
return contact_link || {};
|
||||
}
|
||||
|
||||
setup_listener() {
|
||||
frappe.realtime.on(`call_${this.call_log.id}_ended`, call_log => {
|
||||
this.call_ended(call_log);
|
||||
});
|
||||
|
||||
frappe.realtime.on(`call_${this.call_log.id}_missed`, call_log => {
|
||||
this.call_ended(call_log, true);
|
||||
});
|
||||
}
|
||||
|
||||
clear_listeners() {
|
||||
frappe.realtime.off(`call_${this.call_log.id}_ended`);
|
||||
frappe.realtime.off(`call_${this.call_log.id}_missed`);
|
||||
}
|
||||
|
||||
setup_call_details() {
|
||||
this.caller_info = $(`<div></div>`);
|
||||
this.call_details = new frappe.ui.FieldGroup({
|
||||
fields: [{
|
||||
'fieldname': 'name',
|
||||
'label': 'Name',
|
||||
'default': this.get_caller_name() || __('Unknown Caller'),
|
||||
@ -19,17 +112,17 @@ class CallPopup {
|
||||
}, {
|
||||
'fieldtype': 'Button',
|
||||
'label': __('Open Contact'),
|
||||
'click': () => frappe.set_route('Form', 'Contact', this.call_log.contact),
|
||||
'depends_on': () => this.call_log.contact
|
||||
}, {
|
||||
'fieldtype': 'Button',
|
||||
'label': __('Open Lead'),
|
||||
'click': () => frappe.set_route('Form', 'Lead', this.call_log.lead),
|
||||
'depends_on': () => this.call_log.lead
|
||||
'click': () => frappe.set_route('Form', 'Contact', this.get_contact_link().link_name),
|
||||
'depends_on': () => this.get_caller_name()
|
||||
}, {
|
||||
'fieldtype': 'Button',
|
||||
'label': __('Create New Contact'),
|
||||
'click': () => frappe.new_doc('Contact', { 'mobile_no': this.caller_number }),
|
||||
'click': this.create_new_contact.bind(this),
|
||||
'depends_on': () => !this.get_caller_name()
|
||||
}, {
|
||||
'fieldtype': 'Button',
|
||||
'label': __('Create New Customer'),
|
||||
'click': this.create_new_customer.bind(this),
|
||||
'depends_on': () => !this.get_caller_name()
|
||||
}, {
|
||||
'fieldtype': 'Button',
|
||||
@ -44,26 +137,9 @@ class CallPopup {
|
||||
'fieldtype': 'Data',
|
||||
'default': this.caller_number,
|
||||
'read_only': 1
|
||||
}, {
|
||||
'fielname': 'last_interaction',
|
||||
'fieldtype': 'Section Break',
|
||||
'label': __('Activity'),
|
||||
'depends_on': () => this.get_caller_name()
|
||||
}, {
|
||||
'fieldtype': 'Small Text',
|
||||
'label': __('Last Issue'),
|
||||
'fieldname': 'last_issue',
|
||||
'read_only': true,
|
||||
'depends_on': () => this.call_log.contact,
|
||||
'default': `<i class="text-muted">${__('No issue has been raised by the caller.')}<i>`
|
||||
}, {
|
||||
'fieldtype': 'Small Text',
|
||||
'label': __('Last Communication'),
|
||||
'fieldname': 'last_communication',
|
||||
'read_only': true,
|
||||
'default': `<i class="text-muted">${__('No communication found.')}<i>`
|
||||
}, {
|
||||
'fieldtype': 'Section Break',
|
||||
'hide_border': 1,
|
||||
}, {
|
||||
'fieldtype': 'Small Text',
|
||||
'label': __('Call Summary'),
|
||||
@ -72,7 +148,7 @@ class CallPopup {
|
||||
'fieldtype': 'Button',
|
||||
'label': __('Save'),
|
||||
'click': () => {
|
||||
const call_summary = this.dialog.get_value('call_summary');
|
||||
const call_summary = this.call_details.get_value('call_summary');
|
||||
if (!call_summary) return;
|
||||
frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', {
|
||||
'call_log': this.call_log.name,
|
||||
@ -94,108 +170,42 @@ class CallPopup {
|
||||
});
|
||||
}
|
||||
}],
|
||||
body: this.caller_info
|
||||
});
|
||||
this.set_call_status();
|
||||
this.dialog.get_close_btn().show();
|
||||
this.make_last_interaction_section();
|
||||
this.dialog.$body.addClass('call-popup');
|
||||
this.dialog.set_secondary_action(this.close_modal.bind(this));
|
||||
frappe.utils.play_sound('incoming-call');
|
||||
this.dialog.show();
|
||||
this.call_details.make();
|
||||
}
|
||||
|
||||
set_indicator(color, blink=false) {
|
||||
let classes = `indicator ${color} ${blink ? 'blink': ''}`;
|
||||
this.dialog.header.find('.indicator').attr('class', classes);
|
||||
is_known_caller() {
|
||||
return Boolean(this.get_caller_name());
|
||||
}
|
||||
|
||||
set_call_status(call_status) {
|
||||
let title = '';
|
||||
call_status = call_status || this.call_log.status;
|
||||
if (['Ringing'].includes(call_status) || !call_status) {
|
||||
title = __('Incoming call from {0}', [this.get_caller_name() || this.caller_number]);
|
||||
this.set_indicator('blue', true);
|
||||
} else if (call_status === 'In Progress') {
|
||||
title = __('Call Connected');
|
||||
this.set_indicator('yellow');
|
||||
} else if (call_status === 'Missed') {
|
||||
this.set_indicator('red');
|
||||
title = __('Call Missed');
|
||||
} else if (['Completed', 'Disconnected'].includes(call_status)) {
|
||||
this.set_indicator('red');
|
||||
title = __('Call Disconnected');
|
||||
} else {
|
||||
this.set_indicator('blue');
|
||||
title = call_status;
|
||||
}
|
||||
this.dialog.set_title(title);
|
||||
create_new_customer() {
|
||||
// to avoid quick entry form
|
||||
const new_customer = frappe.model.get_new_doc('Customer');
|
||||
new_customer.mobile_no = this.caller_number;
|
||||
frappe.set_route('Form', new_customer.doctype, new_customer.name);
|
||||
}
|
||||
|
||||
update_call_log(call_log) {
|
||||
this.call_log = call_log;
|
||||
this.set_call_status();
|
||||
}
|
||||
|
||||
close_modal() {
|
||||
this.dialog.hide();
|
||||
delete erpnext.call_popup;
|
||||
}
|
||||
|
||||
call_disconnected(call_log) {
|
||||
frappe.utils.play_sound('call-disconnect');
|
||||
this.update_call_log(call_log);
|
||||
setTimeout(() => {
|
||||
if (!this.dialog.get_value('call_summary')) {
|
||||
this.close_modal();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
make_last_interaction_section() {
|
||||
frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
|
||||
'contact': this.call_log.contact,
|
||||
'lead': this.call_log.lead
|
||||
}).then(data => {
|
||||
const comm_field = this.dialog.get_field('last_communication');
|
||||
if (data.last_communication) {
|
||||
const comm = data.last_communication;
|
||||
comm_field.set_value(comm.content);
|
||||
}
|
||||
|
||||
if (data.last_issue) {
|
||||
const issue = data.last_issue;
|
||||
const issue_field = this.dialog.get_field("last_issue");
|
||||
issue_field.set_value(issue.subject);
|
||||
issue_field.$wrapper.append(`
|
||||
<a class="text-medium" href="/app/issue?customer=${issue.customer}">
|
||||
${__('View all issues from {0}', [issue.customer])}
|
||||
</a>
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_caller_name() {
|
||||
let log = this.call_log;
|
||||
return log.contact_name || log.lead_name;
|
||||
}
|
||||
|
||||
setup_listener() {
|
||||
frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => {
|
||||
this.call_disconnected(call_log);
|
||||
// Remove call disconnect listener after the call is disconnected
|
||||
frappe.realtime.off(`call_${this.call_log.id}_disconnected`);
|
||||
});
|
||||
create_new_contact() {
|
||||
// TODO: fix new_doc, it should accept child table values
|
||||
const new_contact = frappe.model.get_new_doc('Contact');
|
||||
const phone_no = frappe.model.add_child(new_contact, 'Contact Phone', 'phone_nos');
|
||||
phone_no.phone = this.caller_number;
|
||||
phone_no.is_primary_mobile_no = 1;
|
||||
frappe.set_route('Form', new_contact.doctype, new_contact.name);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on('app_ready', function () {
|
||||
frappe.realtime.on('show_call_popup', call_log => {
|
||||
if (!erpnext.call_popup) {
|
||||
erpnext.call_popup = new CallPopup(call_log);
|
||||
let call_popup = erpnext.call_popup;
|
||||
if (call_popup && call_log.name === call_popup.call_log.name) {
|
||||
call_popup.update_call_log(call_log);
|
||||
call_popup.dialog.show();
|
||||
} else {
|
||||
erpnext.call_popup.update_call_log(call_log);
|
||||
erpnext.call_popup.dialog.show();
|
||||
erpnext.call_popup = new CallPopup(call_log);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window.CallPopup = CallPopup;
|
||||
|
@ -1,32 +1,31 @@
|
||||
<div class="call-detail-wrapper">
|
||||
<div class="left-arrow"></div>
|
||||
<div class="head text-muted">
|
||||
<div class="head flex justify-between">
|
||||
<div>
|
||||
<span class="bold"> {{ type }} Call</span>
|
||||
{% if (duration) %}
|
||||
<span class="text-muted"> • {{ frappe.format(duration, { fieldtype: "Duration" }) }}</span>
|
||||
{% endif %}
|
||||
<span class="text-muted"> • {{ comment_when(creation) }}</span>
|
||||
</div>
|
||||
<span>
|
||||
<i class="fa fa-phone"> </i>
|
||||
<span>
|
||||
<span> {{ type }} Call</span>
|
||||
-
|
||||
<span> {{ frappe.format(duration, { fieldtype: "Duration" }) }}</span>
|
||||
-
|
||||
<span> {{ comment_when(creation) }}</span>
|
||||
-
|
||||
<!-- <span> {{ status }}</span>
|
||||
- -->
|
||||
<a class="text-muted" href="#Form/Call Log/{{name}}">Details</a>
|
||||
{% if (show_call_button) { %}
|
||||
<a class="pull-right">Callback</a>
|
||||
{% } %}
|
||||
<a class="action-btn" href="/app/call-log/{{ name }}" title="{{ __("Open Call Log") }}">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-link-url" class="like-icon"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="body padding">
|
||||
|
||||
|
||||
<div class="body pt-3">
|
||||
{% if (type === "Incoming") { %}
|
||||
<span> Incoming call from {{ from }}, received by {{ to }}</span>
|
||||
{% } else { %}
|
||||
<span> Outgoing Call made by {{ from }} to {{ to }}</span>
|
||||
{% } %}
|
||||
<hr>
|
||||
<div class="summary">
|
||||
<div class="summary pt-3">
|
||||
{% if (summary) { %}
|
||||
<span>{{ summary }}</span>
|
||||
<i>{{ summary }}</i>
|
||||
{% } else { %}
|
||||
<i class="text-muted">{{ __("No Summary") }}</i>
|
||||
{% } %}
|
||||
|
@ -1,9 +0,0 @@
|
||||
.call-popup {
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.for-description {
|
||||
max-height: 250px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
21
erpnext/public/scss/call_popup.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.call-popup {
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.for-description {
|
||||
max-height: 250px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
audio {
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
background-color: var(--control-bg);
|
||||
border-radius: var(--border-radius-sm);
|
||||
&-webkit-media-controls-panel {
|
||||
background: var(--control-bg);
|
||||
}
|
||||
outline: none;
|
||||
}
|
@ -20,11 +20,13 @@ from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds
|
||||
|
||||
def validate_einvoice_fields(doc):
|
||||
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
|
||||
invalid_doctype = doc.doctype not in ['Sales Invoice']
|
||||
invalid_doctype = doc.doctype != 'Sales Invoice'
|
||||
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
|
||||
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
||||
no_taxes_applied = len(doc.get('taxes', [])) == 0
|
||||
|
||||
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return
|
||||
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
|
||||
return
|
||||
|
||||
if doc.docstatus == 0 and doc._action == 'save':
|
||||
if doc.irn:
|
||||
@ -303,7 +305,7 @@ def validate_mandatory_fields(invoice):
|
||||
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
|
||||
title=_('Missing Fields')
|
||||
)
|
||||
if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
|
||||
if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
|
||||
frappe.throw(
|
||||
_('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
|
||||
title=_('Missing Fields')
|
||||
@ -444,6 +446,8 @@ class GSPConnector():
|
||||
def get_credentials(self):
|
||||
if self.invoice:
|
||||
gstin = self.get_seller_gstin()
|
||||
if not self.e_invoice_settings.enable:
|
||||
frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
|
||||
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
|
||||
else:
|
||||
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
|
||||
@ -817,4 +821,4 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
||||
@frappe.whitelist()
|
||||
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
|
@ -23,8 +23,10 @@ class ProductQuery:
|
||||
self.cart_settings = frappe.get_doc("Shopping Cart Settings")
|
||||
self.page_length = self.settings.products_per_page or 20
|
||||
self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route']
|
||||
self.filters = [['show_in_website', '=', 1]]
|
||||
self.or_filters = []
|
||||
self.filters = []
|
||||
self.or_filters = [['show_in_website', '=', 1]]
|
||||
if not self.settings.get('hide_variants'):
|
||||
self.or_filters.append(['show_variant_in_website', '=', 1])
|
||||
|
||||
def query(self, attributes=None, fields=None, search_term=None, start=0):
|
||||
"""Summary
|
||||
@ -73,7 +75,8 @@ class ProductQuery:
|
||||
|
||||
for item in result:
|
||||
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
|
||||
item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
|
||||
if product_info:
|
||||
item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
|
||||
|
||||
return result
|
||||
|
||||
|
@ -132,7 +132,7 @@ erpnext.stock.ItemDashboard = Class.extend({
|
||||
var message = __("No Stock Available Currently");
|
||||
this.content.find('.result').css('text-align', 'center');
|
||||
|
||||
$(`<div class='text-muted' style='margin: 20px 5px; font-weight: lighter;'>
|
||||
$(`<div class='text-muted' style='margin: 20px 5px;'>
|
||||
${message} </div>`).appendTo(this.result);
|
||||
}
|
||||
},
|
||||
|
@ -2,7 +2,26 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Call Log', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
refresh: function(frm) {
|
||||
frm.events.setup_recording_audio_control(frm);
|
||||
const incoming_call = frm.doc.type == 'Incoming';
|
||||
frm.add_custom_button(incoming_call ? __('Callback'): __('Call Again'), () => {
|
||||
const number = incoming_call ? frm.doc.from : frm.doc.to;
|
||||
frappe.phone_call.handler(number, frm);
|
||||
});
|
||||
},
|
||||
setup_recording_audio_control(frm) {
|
||||
const recording_wrapper = frm.get_field('recording_html').$wrapper;
|
||||
if (!frm.doc.recording_url || frm.doc.recording_url == 'null') {
|
||||
recording_wrapper.empty();
|
||||
} else {
|
||||
recording_wrapper.addClass('input-max-width');
|
||||
recording_wrapper.html(`
|
||||
<audio
|
||||
controls
|
||||
src="${frm.doc.recording_url}">
|
||||
</audio>
|
||||
`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -5,6 +5,7 @@
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"call_details_section",
|
||||
"id",
|
||||
"from",
|
||||
"to",
|
||||
@ -21,20 +22,9 @@
|
||||
"section_break_11",
|
||||
"summary",
|
||||
"section_break_19",
|
||||
"links",
|
||||
"column_break_3",
|
||||
"section_break_5"
|
||||
"links"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Call Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "id",
|
||||
"fieldtype": "Data",
|
||||
@ -75,6 +65,7 @@
|
||||
{
|
||||
"fieldname": "recording_url",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Recording URL"
|
||||
},
|
||||
{
|
||||
@ -112,13 +103,13 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "summary",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Call Summary"
|
||||
"fieldtype": "Small Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
"hide_border": 1,
|
||||
"label": "Call Summary"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_time",
|
||||
@ -138,12 +129,17 @@
|
||||
"label": "Customer",
|
||||
"options": "Customer",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "call_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Call Details"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-13 12:28:20.288985",
|
||||
"modified": "2021-02-08 14:23:28.744844",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Telephony",
|
||||
"name": "Call Log",
|
||||
|
@ -165,6 +165,8 @@ def get_linked_call_logs(doctype, docname):
|
||||
for log in logs:
|
||||
log.show_call_button = 0
|
||||
timeline_contents.append({
|
||||
'icon': 'call',
|
||||
'is_card': True,
|
||||
'creation': log.creation,
|
||||
'template': 'call_link',
|
||||
'template_data': log
|
||||
|
@ -62,6 +62,9 @@
|
||||
<div class="col-8">
|
||||
{% if doc.items %}
|
||||
<div class="place-order-container">
|
||||
<a class="btn btn-primary-light mr-2" href="/all-products">
|
||||
{{ _("Continue Shopping") }}
|
||||
</a>
|
||||
{% if cart_settings.enable_checkout %}
|
||||
<button class="btn btn-primary btn-place-order" type="button">
|
||||
{{ _("Place Order") }}
|
||||
|