Merge branch 'develop' into tcs_calculation

This commit is contained in:
Saqib Ansari 2021-02-23 15:55:36 +05:30
commit 2fb3647a4c
56 changed files with 411 additions and 327 deletions

View File

@ -30,6 +30,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Reference Document Type", "label": "Reference Document Type",
"options": "DocType", "options": "DocType",
"read_only_depends_on": "eval:!doc.__islocal",
"reqd": 1 "reqd": 1
}, },
{ {
@ -48,7 +49,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-03-22 20:34:39.805728", "modified": "2021-02-08 16:37:53.936656",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting Dimension", "name": "Accounting Dimension",

View File

@ -29,6 +29,16 @@ class AccountingDimension(Document):
if exists and self.is_new(): if exists and self.is_new():
frappe.throw("Document Type already used as a dimension") 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): def after_insert(self):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)

View File

@ -108,7 +108,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-12-16 15:27:23.659285", "modified": "2021-02-03 12:04:58.678402",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting Dimension Filter", "name": "Accounting Dimension Filter",
@ -125,6 +125,30 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 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, "quick_entry": 1,

View File

@ -88,19 +88,19 @@ class PaymentReconciliation(Document):
voucher_type = ('Sales Invoice' voucher_type = ('Sales Invoice'
if self.party_type == 'Customer' else "Purchase 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, return frappe.db.sql(""" SELECT 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, (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount,
account_currency as currency account_currency as currency
FROM `tab{doc}`, `tabGL Entry` FROM `tab{doc}` doc, `tabGL Entry` gl
WHERE WHERE
(`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no) (doc.name = gl.against_voucher or doc.name = gl.voucher_no)
and `tab{doc}`.{party_type_field} = %(party)s and doc.{party_type_field} = %(party)s
and `tab{doc}`.is_return = 1 and `tab{doc}`.return_against IS NULL and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
and `tabGL Entry`.against_voucher_type = %(voucher_type)s and gl.against_voucher_type = %(voucher_type)s
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s and doc.docstatus = 1 and gl.party = %(party)s
and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s and gl.party_type = %(party_type)s and gl.account = %(account)s
and `tabGL Entry`.is_cancelled = 0 and gl.is_cancelled = 0
GROUP BY `tab{doc}`.name GROUP BY doc.name
Having Having
amount > 0 amount > 0
""".format( """.format(
@ -113,7 +113,7 @@ class PaymentReconciliation(Document):
'party_type': self.party_type, 'party_type': self.party_type,
'voucher_type': voucher_type, 'voucher_type': voucher_type,
'account': self.receivable_payable_account 'account': self.receivable_payable_account
}, as_dict=1) }, as_dict=1, debug=1)
def add_payment_entries(self, entries): def add_payment_entries(self, entries):
self.set('payments', []) self.set('payments', [])

View File

@ -20,11 +20,16 @@ class POSClosingEntry(StatusUpdater):
self.validate_pos_invoices() self.validate_pos_invoices()
def validate_pos_closing(self): def validate_pos_closing(self):
user = frappe.get_all("POS Closing Entry", user = frappe.db.sql("""
filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile }, SELECT name FROM `tabPOS Closing Entry`
or_filters = { WHERE
"period_start_date": ("between", [self.period_start_date, self.period_end_date]), user = %(user)s AND docstatus = 1 AND pos_profile = %(profile)s AND
"period_end_date": ("between", [self.period_start_date, self.period_end_date]) (period_start_date between %(start)s and %(end)s OR period_end_date between %(start)s and %(end)s)
""", {
'user': self.user,
'profile': self.pos_profile,
'start': self.period_start_date,
'end': self.period_end_date
}) })
if user: if user:

View File

@ -212,8 +212,8 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
invoice_by_customer = get_invoice_customer_map(invoices) invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 5 and closing_entry: if len(invoices) >= 5 and closing_entry:
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
else: else:
create_merge_logs(invoice_by_customer, closing_entry) create_merge_logs(invoice_by_customer, closing_entry)
@ -225,8 +225,8 @@ def unconsolidate_pos_invoices(closing_entry):
) )
if len(merge_logs) >= 5: if len(merge_logs) >= 5:
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
else: else:
cancel_merge_logs(merge_logs, closing_entry) cancel_merge_logs(merge_logs, closing_entry)

View File

@ -52,8 +52,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
row = { row = {
'item_code': d.item_code, 'item_code': d.item_code,
'item_name': item_record.item_name, 'item_name': item_record.item_name if item_record else d.item_name,
'item_group': item_record.item_group, 'item_group': item_record.item_group if item_record else d.item_group,
'description': d.description, 'description': d.description,
'invoice': d.parent, 'invoice': d.parent,
'posting_date': d.posting_date, '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`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `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`.sales_order, `tabSales Invoice Item`.delivery_note,
`tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
`tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,

View File

@ -1521,6 +1521,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.flags.ignore_validate_update_after_submit = True parent.flags.ignore_validate_update_after_submit = True
parent.set_qty_as_per_stock_uom() parent.set_qty_as_per_stock_uom()
parent.calculate_taxes_and_totals() parent.calculate_taxes_and_totals()
parent.set_total_in_words()
if parent_doctype == "Sales Order": if parent_doctype == "Sales Order":
make_packing_list(parent) make_packing_list(parent)
parent.set_gross_profit() parent.set_gross_profit()

View File

@ -313,7 +313,7 @@ class StockController(AccountsController):
return serialized_items return serialized_items
def validate_warehouse(self): def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
warehouses = list(set([d.warehouse for d in warehouses = list(set([d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)])) self.get("items") if getattr(d, "warehouse", None)]))
@ -329,6 +329,7 @@ class StockController(AccountsController):
warehouses.extend(from_warehouse) warehouses.extend(from_warehouse)
for w in warehouses: for w in warehouses:
validate_disabled_warehouse(w)
validate_warehouse_company(w, self.company) validate_warehouse_company(w, self.company)
def update_billing_percentage(self, update_modified=True): def update_billing_percentage(self, update_modified=True):

View File

@ -352,7 +352,7 @@ def get_lead_with_phone_number(number):
leads = frappe.get_all('Lead', or_filters={ leads = frappe.get_all('Lead', or_filters={
'phone': ['like', '%{}'.format(number)], 'phone': ['like', '%{}'.format(number)],
'mobile_no': ['like', '%{}'.format(number)] 'mobile_no': ['like', '%{}'.format(number)]
}, limit=1) }, limit=1, order_by="creation DESC")
lead = leads[0].name if leads else None lead = leads[0].name if leads else None

View File

@ -139,12 +139,13 @@ def create_inpatient(patient):
inpatient_record.phone = patient_obj.phone inpatient_record.phone = patient_obj.phone
inpatient_record.inpatient = "Scheduled" inpatient_record.inpatient = "Scheduled"
inpatient_record.scheduled_date = today() inpatient_record.scheduled_date = today()
inpatient_record.company = "_Test Company"
return inpatient_record return inpatient_record
def get_healthcare_service_unit(unit_name=None): def get_healthcare_service_unit(unit_name=None):
if not unit_name: 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: else:
service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name}) service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name})

View File

@ -119,7 +119,7 @@ def create_records(patient):
ip_record.expected_length_of_stay = 0 ip_record.expected_length_of_stay = 0
ip_record.save() ip_record.save()
ip_record.reload() 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()) admit_patient(ip_record, service_unit, now_datetime())
ipmo = create_ipmo(patient) ipmo = create_ipmo(patient)

View File

@ -78,7 +78,7 @@ website_generators = ["Item Group", "Item", "BOM", "Sales Partner",
"Job Opening", "Student Admission"] "Job Opening", "Student Admission"]
website_context = { website_context = {
"favicon": "/assets/erpnext/images/favicon.png", "favicon": "/assets/erpnext/images/erpnext-favicon.svg",
"splash_image": "/assets/erpnext/images/erpnext-logo.svg" "splash_image": "/assets/erpnext/images/erpnext-logo.svg"
} }

View File

@ -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 = 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.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
employee.company_email = "test@example.com" employee.company_email = "test@example.com"
employee.company = "_Test Company"
employee.save() employee.save()
from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders 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`") frappe.db.sql("delete from `tabEmail Queue`")

View File

@ -16,7 +16,7 @@ class JobOffer(Document):
def validate(self): def validate(self):
self.validate_vacancies() self.validate_vacancies()
job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant}) job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant, "docstatus": ["!=", 2]})
if job_offer and job_offer != self.name: if job_offer and job_offer != self.name:
frappe.throw(_("Job Offer: {0} is already for Job Applicant: {1}").format(frappe.bold(job_offer), frappe.bold(self.job_applicant))) frappe.throw(_("Job Offer: {0} is already for Job Applicant: {1}").format(frappe.bold(job_offer), frappe.bold(self.job_applicant)))

View File

@ -1,5 +1,6 @@
frappe.listview_settings['Leave Application'] = { frappe.listview_settings['Leave Application'] = {
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"], add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
has_indicator_for_draft: 1,
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status === "Approved") { if (doc.status === "Approved") {
return [__("Approved"), "green", "status,=,Approved"]; return [__("Approved"), "green", "status,=,Approved"];

View File

@ -36,6 +36,8 @@ def execute(filters=None):
conditions, filters = get_conditions(filters) conditions, filters = get_conditions(filters)
columns, days = get_columns(filters) columns, days = get_columns(filters)
att_map = get_attendance_list(conditions, filters) att_map = get_attendance_list(conditions, filters)
if not att_map:
return columns, [], None, None
if filters.group_by: if filters.group_by:
emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company) emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company)
@ -65,9 +67,13 @@ def execute(filters=None):
if filters.group_by: if filters.group_by:
emp_att_map = {} emp_att_map = {}
for parameter in group_by_parameters: for parameter in group_by_parameters:
data.append([ "<b>"+ parameter + "</b>"]) emp_map_set = set([key for key in emp_map[parameter].keys()])
record, aaa = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list) att_map_set = set([key for key in att_map.keys()])
emp_att_map.update(aaa) 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 data += record
else: else:
record, emp_att_map = add_data(emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list) record, emp_att_map = add_data(emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
@ -237,6 +243,9 @@ def get_attendance_list(conditions, filters):
status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" % status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" %
conditions, filters, as_dict=1) conditions, filters, as_dict=1)
if not attendance_list:
msgprint(_("No attendance record found"), alert=True, indicator="orange")
att_map = {} att_map = {}
for d in attendance_list: for d in attendance_list:
att_map.setdefault(d.employee, frappe._dict()).setdefault(d.day_of_month, "") att_map.setdefault(d.employee, frappe._dict()).setdefault(d.day_of_month, "")

View File

@ -544,7 +544,7 @@ class TestWorkOrder(unittest.TestCase):
expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4} expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4}
for row in ste3.items: for row in ste3.items:
self.assertEquals(row.qty, expected_qty.get(row.item_code)) self.assertEquals(row.qty, expected_qty.get(row.item_code))
ste_cancel_list.reverse()
for ste_doc in ste_cancel_list: for ste_doc in ste_cancel_list:
ste_doc.cancel() ste_doc.cancel()
@ -586,7 +586,7 @@ class TestWorkOrder(unittest.TestCase):
for ste_row in ste2.items: for ste_row in ste2.items:
if itemwise_qty.get(ste_row.item_code) and ste_row.s_warehouse: if itemwise_qty.get(ste_row.item_code) and ste_row.s_warehouse:
self.assertEquals(ste_row.qty, itemwise_qty.get(ste_row.item_code) / 2) self.assertEquals(ste_row.qty, itemwise_qty.get(ste_row.item_code) / 2)
ste_cancel_list.reverse()
for ste_doc in ste_cancel_list: for ste_doc in ste_cancel_list:
ste_doc.cancel() ste_doc.cancel()

View File

@ -386,6 +386,7 @@ def get_items(filters=None, search=None):
r.description = r.web_long_description or r.description r.description = r.web_long_description or r.description
r.image = r.website_image or r.image 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') product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
return results return results

View File

@ -288,7 +288,7 @@ def make_sales_invoice(source_name, item_code=None, customer=None):
def make_salary_slip(source_name, target_doc=None): def make_salary_slip(source_name, target_doc=None):
target = frappe.new_doc("Salary Slip") target = frappe.new_doc("Salary Slip")
set_missing_values(source_name, target) 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 return target

View File

@ -2,7 +2,7 @@
"css/erpnext.css": [ "css/erpnext.css": [
"public/less/erpnext.less", "public/less/erpnext.less",
"public/less/hub.less", "public/less/hub.less",
"public/less/call_popup.less", "public/scss/call_popup.scss",
"public/scss/point-of-sale.scss" "public/scss/point-of-sale.scss"
], ],
"css/marketplace.css": [ "css/marketplace.css": [

View File

@ -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

View File

@ -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

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -7,10 +7,103 @@ class CallPopup {
} }
make() { make() {
frappe.utils.play_sound('incoming-call');
this.dialog = new frappe.ui.Dialog({ this.dialog = new frappe.ui.Dialog({
'static': true, 'static': true,
'minimizable': true, 'minimizable': true
'fields': [{ });
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', 'fieldname': 'name',
'label': 'Name', 'label': 'Name',
'default': this.get_caller_name() || __('Unknown Caller'), 'default': this.get_caller_name() || __('Unknown Caller'),
@ -19,17 +112,17 @@ class CallPopup {
}, { }, {
'fieldtype': 'Button', 'fieldtype': 'Button',
'label': __('Open Contact'), 'label': __('Open Contact'),
'click': () => frappe.set_route('Form', 'Contact', this.call_log.contact), 'click': () => frappe.set_route('Form', 'Contact', this.get_contact_link().link_name),
'depends_on': () => this.call_log.contact 'depends_on': () => this.get_caller_name()
}, {
'fieldtype': 'Button',
'label': __('Open Lead'),
'click': () => frappe.set_route('Form', 'Lead', this.call_log.lead),
'depends_on': () => this.call_log.lead
}, { }, {
'fieldtype': 'Button', 'fieldtype': 'Button',
'label': __('Create New Contact'), '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() 'depends_on': () => !this.get_caller_name()
}, { }, {
'fieldtype': 'Button', 'fieldtype': 'Button',
@ -44,26 +137,9 @@ class CallPopup {
'fieldtype': 'Data', 'fieldtype': 'Data',
'default': this.caller_number, 'default': this.caller_number,
'read_only': 1 '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', 'fieldtype': 'Section Break',
'hide_border': 1,
}, { }, {
'fieldtype': 'Small Text', 'fieldtype': 'Small Text',
'label': __('Call Summary'), 'label': __('Call Summary'),
@ -72,7 +148,7 @@ class CallPopup {
'fieldtype': 'Button', 'fieldtype': 'Button',
'label': __('Save'), 'label': __('Save'),
'click': () => { 'click': () => {
const call_summary = this.dialog.get_value('call_summary'); const call_summary = this.call_details.get_value('call_summary');
if (!call_summary) return; if (!call_summary) return;
frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', { frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', {
'call_log': this.call_log.name, 'call_log': this.call_log.name,
@ -94,108 +170,42 @@ class CallPopup {
}); });
} }
}], }],
body: this.caller_info
}); });
this.set_call_status(); this.call_details.make();
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();
} }
set_indicator(color, blink=false) { is_known_caller() {
let classes = `indicator ${color} ${blink ? 'blink': ''}`; return Boolean(this.get_caller_name());
this.dialog.header.find('.indicator').attr('class', classes);
} }
set_call_status(call_status) { create_new_customer() {
let title = ''; // to avoid quick entry form
call_status = call_status || this.call_log.status; const new_customer = frappe.model.get_new_doc('Customer');
if (['Ringing'].includes(call_status) || !call_status) { new_customer.mobile_no = this.caller_number;
title = __('Incoming call from {0}', [this.get_caller_name() || this.caller_number]); frappe.set_route('Form', new_customer.doctype, new_customer.name);
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);
} }
update_call_log(call_log) { create_new_contact() {
this.call_log = call_log; // TODO: fix new_doc, it should accept child table values
this.set_call_status(); 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;
close_modal() { phone_no.is_primary_mobile_no = 1;
this.dialog.hide(); frappe.set_route('Form', new_contact.doctype, new_contact.name);
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`);
});
} }
} }
$(document).on('app_ready', function () { $(document).on('app_ready', function () {
frappe.realtime.on('show_call_popup', call_log => { frappe.realtime.on('show_call_popup', call_log => {
if (!erpnext.call_popup) { let call_popup = erpnext.call_popup;
erpnext.call_popup = new CallPopup(call_log); if (call_popup && call_log.name === call_popup.call_log.name) {
call_popup.update_call_log(call_log);
call_popup.dialog.show();
} else { } else {
erpnext.call_popup.update_call_log(call_log); erpnext.call_popup = new CallPopup(call_log);
erpnext.call_popup.dialog.show();
} }
}); });
}); });
window.CallPopup = CallPopup;

View File

@ -1,32 +1,31 @@
<div class="call-detail-wrapper"> <div class="call-detail-wrapper">
<div class="left-arrow"></div> <div class="head flex justify-between">
<div class="head text-muted"> <div>
<span> <span class="bold"> {{ type }} Call</span>
<i class="fa fa-phone"> </i> {% if (duration) %}
<span> <span class="text-muted"> • {{ frappe.format(duration, { fieldtype: "Duration" }) }}</span>
<span> {{ type }} Call</span> {% endif %}
- <span class="text-muted"> • {{ comment_when(creation) }}</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>
{% } %}
</div> </div>
<div class="body padding"> <span>
<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 pt-3">
{% if (type === "Incoming") { %} {% if (type === "Incoming") { %}
<span> Incoming call from {{ from }}, received by {{ to }}</span> <span> Incoming call from {{ from }}, received by {{ to }}</span>
{% } else { %} {% } else { %}
<span> Outgoing Call made by {{ from }} to {{ to }}</span> <span> Outgoing Call made by {{ from }} to {{ to }}</span>
{% } %} {% } %}
<hr> <div class="summary pt-3">
<div class="summary">
{% if (summary) { %} {% if (summary) { %}
<span>{{ summary }}</span> <i>{{ summary }}</i>
{% } else { %} {% } else { %}
<i class="text-muted">{{ __("No Summary") }}</i> <i class="text-muted">{{ __("No Summary") }}</i>
{% } %} {% } %}

View File

@ -1,9 +0,0 @@
.call-popup {
a:hover {
text-decoration: underline;
}
.for-description {
max-height: 250px;
overflow: scroll;
}
}

View 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;
}

View File

@ -20,11 +20,13 @@ from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds
def validate_einvoice_fields(doc): def validate_einvoice_fields(doc):
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) 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'] 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') 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.docstatus == 0 and doc._action == 'save':
if doc.irn: 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.'), _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
title=_('Missing Fields') 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( frappe.throw(
_('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'), _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
title=_('Missing Fields') title=_('Missing Fields')
@ -444,6 +446,8 @@ class GSPConnector():
def get_credentials(self): def get_credentials(self):
if self.invoice: if self.invoice:
gstin = self.get_seller_gstin() 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) credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
else: else:
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None

View File

@ -32,6 +32,10 @@ def execute(filters=None):
data = [] data = []
columns = get_columns() columns = get_columns()
conditions = ""
if filters.supplier_group:
conditions += "AND s.supplier_group = %s" %frappe.db.escape(filters.get("supplier_group"))
data = frappe.db.sql(""" data = frappe.db.sql("""
SELECT SELECT
s.supplier_group as "supplier_group", s.supplier_group as "supplier_group",
@ -46,13 +50,15 @@ def execute(filters=None):
AND s.irs_1099 = 1 AND s.irs_1099 = 1
AND gl.fiscal_year = %(fiscal_year)s AND gl.fiscal_year = %(fiscal_year)s
AND gl.party_type = "Supplier" AND gl.party_type = "Supplier"
AND gl.company = %(company)s
{conditions}
GROUP BY GROUP BY
gl.party gl.party
ORDER BY ORDER BY
gl.party DESC gl.party DESC""".format(conditions=conditions), {
""", {
"fiscal_year": filters.fiscal_year, "fiscal_year": filters.fiscal_year,
"supplier_group": filters.supplier_group,
"company": filters.company "company": filters.company
}, as_dict=True) }, as_dict=True)
@ -79,13 +85,13 @@ def get_columns():
"fieldname": "tax_id", "fieldname": "tax_id",
"label": _("Tax ID"), "label": _("Tax ID"),
"fieldtype": "Data", "fieldtype": "Data",
"width": 120 "width": 200
}, },
{ {
"fieldname": "payments", "fieldname": "payments",
"label": _("Total Payments"), "label": _("Total Payments"),
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 120 "width": 200
} }
] ]

View File

@ -26,7 +26,6 @@ class TestUnitedStates(unittest.TestCase):
make_payment_entry_to_irs_1099_supplier() make_payment_entry_to_irs_1099_supplier()
filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"})
columns, data = execute_1099_report(filters) columns, data = execute_1099_report(filters)
print(columns, data)
expected_row = {'supplier': '_US 1099 Test Supplier', expected_row = {'supplier': '_US 1099 Test Supplier',
'supplier_group': 'Services', 'supplier_group': 'Services',
'payments': 100.0, 'payments': 100.0,

View File

@ -325,6 +325,9 @@ class TestSalesOrder(unittest.TestCase):
create_dn_against_so(so.name, 4) create_dn_against_so(so.name, 4)
make_sales_invoice(so.name) make_sales_invoice(so.name)
prev_total = so.get("base_total")
prev_total_in_words = so.get("base_in_words")
first_item_of_so = so.get("items")[0] first_item_of_so = so.get("items")[0]
trans_item = json.dumps([ trans_item = json.dumps([
{'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \ {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
@ -340,6 +343,12 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(so.get("items")[-1].amount, 1400) self.assertEqual(so.get("items")[-1].amount, 1400)
self.assertEqual(so.status, 'To Deliver and Bill') self.assertEqual(so.status, 'To Deliver and Bill')
updated_total = so.get("base_total")
updated_total_in_words = so.get("base_in_words")
self.assertEqual(updated_total, prev_total+1400)
self.assertNotEqual(updated_total_in_words, prev_total_in_words)
def test_update_child_removing_item(self): def test_update_child_removing_item(self):
so = make_sales_order(**{ so = make_sales_order(**{
"item_list": [{ "item_list": [{

View File

@ -139,7 +139,7 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-03-18 18:10:13.048492", "modified": "2021-02-08 17:01:52.162202",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Customer Group", "name": "Customer Group",
@ -189,6 +189,15 @@
"permlevel": 1, "permlevel": 1,
"read": 1, "read": 1,
"role": "Sales Manager" "role": "Sales Manager"
},
{
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Customer",
"select": 1,
"share": 1
} }
], ],
"search_fields": "parent_customer_group", "search_fields": "parent_customer_group",

View File

@ -214,7 +214,7 @@
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"max_attachments": 3, "max_attachments": 3,
"modified": "2020-12-30 12:57:38.876956", "modified": "2021-02-08 17:02:44.951572",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Item Group", "name": "Item Group",
@ -271,6 +271,15 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts User" "role": "Accounts User"
},
{
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Customer",
"select": 1,
"share": 1
} }
], ],
"search_fields": "parent_item_group", "search_fields": "parent_item_group",

View File

@ -98,8 +98,6 @@ class ItemGroup(NestedSet, WebsiteGenerator):
context.field_filters = filter_engine.get_field_filters() context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_fitlers() context.attribute_filters = filter_engine.get_attribute_fitlers()
print(context.field_filters, context.attribute_filters)
context.update({ context.update({
"parents": get_parent_item_groups(self.parent_item_group), "parents": get_parent_item_groups(self.parent_item_group),
"title": self.name "title": self.name

View File

@ -123,7 +123,7 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-03-18 18:11:36.623555", "modified": "2021-02-08 17:10:03.767426",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Territory", "name": "Territory",
@ -166,6 +166,15 @@
{ {
"read": 1, "read": 1,
"role": "Maintenance User" "role": "Maintenance User"
},
{
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Customer",
"select": 1,
"share": 1
} }
], ],
"search_fields": "parent_territory,territory_manager", "search_fields": "parent_territory,territory_manager",

View File

@ -16,7 +16,6 @@ class ProductFiltersBuilder:
def get_field_filters(self): def get_field_filters(self):
filter_fields = [row.fieldname for row in self.doc.filter_fields] filter_fields = [row.fieldname for row in self.doc.filter_fields]
print('FILTERS', self.doc.filter_fields)
meta = frappe.get_meta('Item') meta = frappe.get_meta('Item')
fields = [df for df in meta.fields if df.fieldname in filter_fields] fields = [df for df in meta.fields if df.fieldname in filter_fields]
@ -53,7 +52,6 @@ class ProductFiltersBuilder:
def get_attribute_fitlers(self): def get_attribute_fitlers(self):
attributes = [row.attribute for row in self.doc.filter_attributes] attributes = [row.attribute for row in self.doc.filter_attributes]
print('ATTRIBUTES', attributes)
attribute_docs = [ attribute_docs = [
frappe.get_doc('Item Attribute', attribute) for attribute in attributes frappe.get_doc('Item Attribute', attribute) for attribute in attributes
] ]

View File

@ -23,8 +23,10 @@ class ProductQuery:
self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.cart_settings = frappe.get_doc("Shopping Cart Settings")
self.page_length = self.settings.products_per_page or 20 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.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.filters = []
self.or_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): def query(self, attributes=None, fields=None, search_term=None, start=0):
"""Summary """Summary
@ -73,6 +75,7 @@ class ProductQuery:
for item in result: for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
return result return result

View File

@ -132,7 +132,7 @@ erpnext.stock.ItemDashboard = Class.extend({
var message = __("No Stock Available Currently"); var message = __("No Stock Available Currently");
this.content.find('.result').css('text-align', 'center'); 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); ${message} </div>`).appendTo(this.result);
} }
}, },

View File

@ -17,7 +17,7 @@ class Bin(Document):
'''Called from erpnext.stock.utils.update_bin''' '''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args) self.update_qty(args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle from erpnext.stock.stock_ledger import update_entries_after, validate_negative_qty_in_future_sle
if not args.get("posting_date"): if not args.get("posting_date"):
args["posting_date"] = nowdate() args["posting_date"] = nowdate()
@ -37,8 +37,8 @@ class Bin(Document):
"sle_id": args.name "sle_id": args.name
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# Update qty_after_transaction in future SLEs of this item and warehouse # Validate negative qty in future transactions
update_qty_in_future_sle(args) validate_negative_qty_in_future_sle(args)
def update_qty(self, args): def update_qty(self, args):
# update the stock values (for current quantities) # update the stock values (for current quantities)

View File

@ -31,7 +31,7 @@ frappe.ui.form.on('Repost Item Valuation', {
} }
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.status == "Failed") { if (frm.doc.status == "Failed" && frm.doc.docstatus==1) {
frm.add_custom_button(__('Restart'), function () { frm.add_custom_button(__('Restart'), function () {
frm.trigger("restart_reposting"); frm.trigger("restart_reposting");
}).addClass("btn-primary"); }).addClass("btn-primary");

View File

@ -27,10 +27,11 @@ class StockLedgerEntry(Document):
def validate(self): def validate(self):
self.flags.ignore_submit_comment = True self.flags.ignore_submit_comment = True
from erpnext.stock.utils import validate_warehouse_company from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
self.validate_mandatory() self.validate_mandatory()
self.validate_item() self.validate_item()
self.validate_batch() self.validate_batch()
validate_disabled_warehouse(self.warehouse)
validate_warehouse_company(self.warehouse, self.company) validate_warehouse_company(self.warehouse, self.company)
self.scrub_posting_time() self.scrub_posting_time()
self.validate_and_set_fiscal_year() self.validate_and_set_fiscal_year()

View File

@ -62,7 +62,7 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
sle.submit() sle.submit()
return sle return sle
def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=False, via_landed_cost_voucher=False): def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False):
if not args and voucher_type and voucher_no: if not args and voucher_type and voucher_no:
args = get_args_for_voucher(voucher_type, voucher_no) args = get_args_for_voucher(voucher_type, voucher_no)
@ -181,7 +181,7 @@ class update_entries_after(object):
self.process_sle(sle) self.process_sle(sle)
if sle.dependant_sle_voucher_detail_no: if sle.dependant_sle_voucher_detail_no:
self.get_dependent_entries_to_fix(entries_to_fix, sle) entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
if self.exceptions: if self.exceptions:
self.raise_exceptions() self.raise_exceptions()
@ -221,13 +221,15 @@ class update_entries_after(object):
excluded_sle=sle.name) excluded_sle=sle.name)
if not dependant_sle: if not dependant_sle:
return return entries_to_fix
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
return return entries_to_fix
elif dependant_sle.item_code != self.item_code \ elif dependant_sle.item_code != self.item_code:
and (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items: if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items:
self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle
return return entries_to_fix
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
self.initialize_previous_data(dependant_sle) self.initialize_previous_data(dependant_sle)
@ -236,7 +238,7 @@ class update_entries_after(object):
future_sle_for_dependant = list(self.get_sle_after_datetime(args)) future_sle_for_dependant = list(self.get_sle_after_datetime(args))
entries_to_fix.extend(future_sle_for_dependant) entries_to_fix.extend(future_sle_for_dependant)
entries_to_fix = sorted(entries_to_fix, key=lambda k: k['timestamp']) return sorted(entries_to_fix, key=lambda k: k['timestamp'])
def process_sle(self, sle): def process_sle(self, sle):
# previous sle data for this warehouse # previous sle data for this warehouse
@ -612,11 +614,11 @@ class update_entries_after(object):
frappe.local.flags.currently_saving): frappe.local.flags.currently_saving):
msg = _("{0} units of {1} needed in {2} to complete this transaction.").format( msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
abs(deficiency), frappe.get_desk_link('Item', self.item_code), abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
frappe.get_desk_link('Warehouse', warehouse)) frappe.get_desk_link('Warehouse', warehouse))
else: else:
msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
abs(deficiency), frappe.get_desk_link('Item', self.item_code), abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
frappe.get_desk_link('Warehouse', warehouse), frappe.get_desk_link('Warehouse', warehouse),
exceptions[0]["posting_date"], exceptions[0]["posting_time"], exceptions[0]["posting_date"], exceptions[0]["posting_time"],
frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"])) frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]))
@ -761,25 +763,6 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
return valuation_rate return valuation_rate
def update_qty_in_future_sle(args, allow_negative_stock=None):
frappe.db.sql("""
update `tabStock Ledger Entry`
set qty_after_transaction = qty_after_transaction + {qty}
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
or (
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
and creation > %(creation)s
)
)
""".format(qty=args.actual_qty), args)
validate_negative_qty_in_future_sle(args, allow_negative_stock)
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \ allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
@ -808,6 +791,7 @@ def get_future_sle_with_negative_qty(args):
and voucher_no != %(voucher_no)s and voucher_no != %(voucher_no)s
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and is_cancelled = 0 and is_cancelled = 0
and qty_after_transaction < 0 and qty_after_transaction + {0} < 0
order by timestamp(posting_date, posting_time) asc
limit 1 limit 1
""", args, as_dict=1) """.format(args.actual_qty), args, as_dict=1)

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe import _ from frappe import _
import json import json
from frappe.utils import flt, cstr, nowdate, nowtime from frappe.utils import flt, cstr, nowdate, nowtime, get_link_to_form
from six import string_types from six import string_types
@ -284,6 +284,10 @@ def is_group_warehouse(warehouse):
if frappe.db.get_value("Warehouse", warehouse, "is_group"): if frappe.db.get_value("Warehouse", warehouse, "is_group"):
frappe.throw(_("Group node warehouse is not allowed to select for transactions")) frappe.throw(_("Group node warehouse is not allowed to select for transactions"))
def validate_disabled_warehouse(warehouse):
if frappe.db.get_value("Warehouse", warehouse, "disabled"):
frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse)))
def update_included_uom_in_report(columns, result, include_uom, conversion_factors): def update_included_uom_in_report(columns, result, include_uom, conversion_factors):
if not include_uom or not conversion_factors: if not include_uom or not conversion_factors:
return return

View File

@ -147,8 +147,7 @@ class IssueAnalytics(object):
self.entries = frappe.db.get_all('Issue', self.entries = frappe.db.get_all('Issue',
fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'], fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'],
filters=filters, filters=filters
debug=1
) )
def get_common_filters(self): def get_common_filters(self):

View File

@ -17,8 +17,11 @@ class TestIssueAnalytics(unittest.TestCase):
current_month_date = getdate() current_month_date = getdate()
last_month_date = add_months(current_month_date, -1) last_month_date = add_months(current_month_date, -1)
self.current_month = str(months[current_month_date.month - 1]).lower() + '_' + str(current_month_date.year) self.current_month = str(months[current_month_date.month - 1]).lower()
self.last_month = str(months[last_month_date.month - 1]).lower() + '_' + str(last_month_date.year) self.last_month = str(months[last_month_date.month - 1]).lower()
if current_month_date.year != last_month_date.year:
self.current_month += '_' + str(current_month_date.year)
self.last_month += '_' + str(last_month_date.year)
def test_issue_analytics(self): def test_issue_analytics(self):
create_service_level_agreements_for_issues() create_service_level_agreements_for_issues()

View File

@ -2,7 +2,26 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Call Log', { 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>
`);
}
}
}); });

View File

@ -5,6 +5,7 @@
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"call_details_section",
"id", "id",
"from", "from",
"to", "to",
@ -21,20 +22,9 @@
"section_break_11", "section_break_11",
"summary", "summary",
"section_break_19", "section_break_19",
"links", "links"
"column_break_3",
"section_break_5"
], ],
"fields": [ "fields": [
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Call Details"
},
{ {
"fieldname": "id", "fieldname": "id",
"fieldtype": "Data", "fieldtype": "Data",
@ -75,6 +65,7 @@
{ {
"fieldname": "recording_url", "fieldname": "recording_url",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1,
"label": "Recording URL" "label": "Recording URL"
}, },
{ {
@ -112,13 +103,13 @@
}, },
{ {
"fieldname": "summary", "fieldname": "summary",
"fieldtype": "Small Text", "fieldtype": "Small Text"
"label": "Call Summary"
}, },
{ {
"fieldname": "section_break_11", "fieldname": "section_break_11",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1 "hide_border": 1,
"label": "Call Summary"
}, },
{ {
"fieldname": "start_time", "fieldname": "start_time",
@ -138,12 +129,17 @@
"label": "Customer", "label": "Customer",
"options": "Customer", "options": "Customer",
"read_only": 1 "read_only": 1
},
{
"fieldname": "call_details_section",
"fieldtype": "Section Break",
"label": "Call Details"
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-01-13 12:28:20.288985", "modified": "2021-02-08 14:23:28.744844",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Telephony", "module": "Telephony",
"name": "Call Log", "name": "Call Log",

View File

@ -165,6 +165,8 @@ def get_linked_call_logs(doctype, docname):
for log in logs: for log in logs:
log.show_call_button = 0 log.show_call_button = 0
timeline_contents.append({ timeline_contents.append({
'icon': 'call',
'is_card': True,
'creation': log.creation, 'creation': log.creation,
'template': 'call_link', 'template': 'call_link',
'template_data': log 'template_data': log

View File

@ -62,6 +62,9 @@
<div class="col-8"> <div class="col-8">
{% if doc.items %} {% if doc.items %}
<div class="place-order-container"> <div class="place-order-container">
<a class="btn btn-primary-light mr-2" href="/all-products">
{{ _("Continue Shopping") }}
</a>
{% if cart_settings.enable_checkout %} {% if cart_settings.enable_checkout %}
<button class="btn btn-primary btn-place-order" type="button"> <button class="btn btn-primary btn-place-order" type="button">
{{ _("Place Order") }} {{ _("Place Order") }}

View File

@ -12,7 +12,6 @@ def get_context(context):
search = frappe.form_dict.search search = frappe.form_dict.search
field_filters = frappe.parse_json(frappe.form_dict.field_filters) field_filters = frappe.parse_json(frappe.form_dict.field_filters)
attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters) attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
print(field_filters, attribute_filters)
start = frappe.parse_json(frappe.form_dict.start) start = frappe.parse_json(frappe.form_dict.start)
else: else:
search = field_filters = attribute_filters = None search = field_filters = attribute_filters = None
@ -30,8 +29,6 @@ def get_context(context):
context.field_filters = filter_engine.get_field_filters() context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_fitlers() context.attribute_filters = filter_engine.get_attribute_fitlers()
print(context.field_filters, context.attribute_filters)
context.product_settings = product_settings context.product_settings = product_settings
context.body_class = "product-page" context.body_class = "product-page"
context.page_length = product_settings.products_per_page or 20 context.page_length = product_settings.products_per_page or 20