Merge branch 'develop' into lcv_multicurrency

This commit is contained in:
Marica 2021-01-25 12:39:16 +05:30 committed by GitHub
commit 62a1caf6c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1863 additions and 688 deletions

View File

@ -132,16 +132,10 @@ def allow_regional(fn):
return caller return caller
def get_last_membership(): def get_last_membership(member):
'''Returns last membership if exists''' '''Returns last membership if exists'''
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1) dict(member=member, paid=1), order_by='to_date desc', limit=1)
return last_membership and last_membership[0] if last_membership:
return last_membership[0]
def is_member():
'''Returns true if the user is still a member'''
last_membership = get_last_membership()
if last_membership and getdate(last_membership.to_date) > getdate():
return True
return False

View File

@ -1861,23 +1861,6 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_json(self): def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice from erpnext.regional.india.e_invoice.utils import make_einvoice
customer_gstin = '27AACCM7806M1Z3'
customer_gstin_dtls = {
'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
company_gstin = '27AAECE4835E1ZR'
company_gstin_dtls = {
'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
# set cache gstin details to avoid fetching details which will require connection to GSP servers
frappe.local.gstin_cache = {}
frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'
si.items = [] si.items = []
@ -1930,12 +1913,12 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(value_details['SgstVal'], total_item_sgst_value) self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
self.assertEqual(value_details['IgstVal'], total_item_igst_value) self.assertEqual(value_details['IgstVal'], total_item_igst_value)
self.assertEqual( calculated_invoice_value = \
value_details['TotInvVal'], value_details['AssVal'] + value_details['CgstVal'] \
value_details['AssVal'] + value_details['CgstVal'] + value_details['SgstVal'] + value_details['IgstVal'] \
+ value_details['SgstVal'] + value_details['IgstVal']
+ value_details['OthChrg'] - value_details['Discount'] + value_details['OthChrg'] - value_details['Discount']
)
self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls']) self.assertTrue(einvoice['EwbDtls'])

View File

@ -53,8 +53,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,
@ -316,6 +316,7 @@ def get_items(filters, additional_query_columns):
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,

View File

@ -655,6 +655,34 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters) return frappe.db.sql(query, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_healthcare_service_units(doctype, txt, searchfield, start, page_len, filters):
query = """
select name
from `tabHealthcare Service Unit`
where
is_group = 0
and company = {company}
and name like {txt}""".format(
company = frappe.db.escape(filters.get('company')), txt = frappe.db.escape('%{0}%'.format(txt)))
if filters and filters.get('inpatient_record'):
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
service_unit = get_current_healthcare_service_unit(filters.get('inpatient_record'))
# if the patient is admitted, then appointments should be allowed against the admission service unit,
# inspite of it being an Inpatient Occupancy service unit
if service_unit:
query += " and (allow_appointments = 1 or name = {service_unit})".format(service_unit = frappe.db.escape(service_unit))
else:
query += " and allow_appointments = 1"
else:
query += " and allow_appointments = 1"
return frappe.db.sql(query, filters)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype, txt, searchfield, start, page_len, filters): def get_tax_template(doctype, txt, searchfield, start, page_len, filters):

View File

@ -8,12 +8,12 @@
"is_mandatory": 0, "is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-14 17:38:27.496696", "modified": "2021-01-21 15:28:52.483839",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create Opportunity", "name": "Create Opportunity",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Opportunity", "reference_document": "Opportunity",
"show_full_form": 0, "show_full_form": 1,
"title": "Create Opportunity", "title": "Create Opportunity",
"validate_action": 1 "validate_action": 1
} }

View File

@ -100,7 +100,6 @@ class ClinicalProcedure(Document):
allow_start = self.set_actual_qty() allow_start = self.set_actual_qty()
if allow_start: if allow_start:
self.db_set('status', 'In Progress') self.db_set('status', 'In Progress')
insert_clinical_procedure_to_medical_record(self)
return 'success' return 'success'
return 'insufficient stock' return 'insufficient stock'
@ -247,21 +246,3 @@ def make_procedure(source_name, target_doc=None):
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doc return doc
def insert_clinical_procedure_to_medical_record(doc):
subject = frappe.bold(_("Clinical Procedure conducted: ")) + cstr(doc.procedure_template) + "<br>"
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
if subject and doc.notes:
subject += '<br/>' + doc.notes
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.start_date
medical_record.reference_doctype = 'Clinical Procedure'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)

View File

@ -264,7 +264,7 @@ def get_filters(entry):
def get_current_healthcare_service_unit(inpatient_record): def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record) ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
if ip_record.inpatient_occupancies: if ip_record.status in ['Admitted', 'Discharge Scheduled'] and ip_record.inpatient_occupancies:
return ip_record.inpatient_occupancies[-1].service_unit return ip_record.inpatient_occupancies[-1].service_unit
return return

View File

@ -142,11 +142,15 @@ def create_inpatient(patient):
return inpatient_record return inpatient_record
def get_healthcare_service_unit(): def get_healthcare_service_unit(unit_name=None):
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) if not unit_name:
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
else:
service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name})
if not service_unit: if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit") service_unit = frappe.new_doc("Healthcare Service Unit")
service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy" service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy"
service_unit.company = "_Test Company" service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type() service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1 service_unit.inpatient_occupancy = 1

View File

@ -359,6 +359,7 @@
{ {
"fieldname": "normal_test_items", "fieldname": "normal_test_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Normal Test Result",
"options": "Normal Test Result", "options": "Normal Test Result",
"print_hide": 1 "print_hide": 1
}, },
@ -380,6 +381,7 @@
{ {
"fieldname": "sensitivity_test_items", "fieldname": "sensitivity_test_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Sensitivity Test Result",
"options": "Sensitivity Test Result", "options": "Sensitivity Test Result",
"print_hide": 1, "print_hide": 1,
"report_hide": 1 "report_hide": 1
@ -529,6 +531,7 @@
{ {
"fieldname": "descriptive_test_items", "fieldname": "descriptive_test_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Descriptive Test Result",
"options": "Descriptive Test Result", "options": "Descriptive Test Result",
"print_hide": 1, "print_hide": 1,
"report_hide": 1 "report_hide": 1
@ -549,13 +552,14 @@
{ {
"fieldname": "organism_test_items", "fieldname": "organism_test_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Organism Test Result",
"options": "Organism Test Result", "options": "Organism Test Result",
"print_hide": 1 "print_hide": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-30 18:18:38.516215", "modified": "2020-11-30 11:04:17.195848",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Lab Test", "name": "Lab Test",

View File

@ -17,11 +17,9 @@ class LabTest(Document):
self.validate_result_values() self.validate_result_values()
self.db_set('submitted_date', getdate()) self.db_set('submitted_date', getdate())
self.db_set('status', 'Completed') self.db_set('status', 'Completed')
insert_lab_test_to_medical_record(self)
def on_cancel(self): def on_cancel(self):
self.db_set('status', 'Cancelled') self.db_set('status', 'Cancelled')
delete_lab_test_from_medical_record(self)
self.reload() self.reload()
def on_update(self): def on_update(self):
@ -330,60 +328,6 @@ def get_employee_by_user_id(user_id):
return frappe.get_doc('Employee', emp_id) return frappe.get_doc('Employee', emp_id)
return None return None
def insert_lab_test_to_medical_record(doc):
table_row = False
subject = cstr(doc.lab_test_name)
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: '))+ doc.practitioner + '<br>'
if doc.normal_test_items:
item = doc.normal_test_items[0]
comment = ''
if item.lab_test_comment:
comment = str(item.lab_test_comment)
table_row = frappe.bold(_('Lab Test Conducted: ')) + item.lab_test_name
if item.lab_test_event:
table_row += frappe.bold(_('Lab Test Event: ')) + item.lab_test_event
if item.result_value:
table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value
if item.normal_range:
table_row += ' ' + _('Normal Range: ') + item.normal_range
table_row += ' ' + comment
elif doc.descriptive_test_items:
item = doc.descriptive_test_items[0]
if item.lab_test_particulars and item.result_value:
table_row = item.lab_test_particulars + ' ' + item.result_value
elif doc.sensitivity_test_items:
item = doc.sensitivity_test_items[0]
if item.antibiotic and item.antibiotic_sensitivity:
table_row = item.antibiotic + ' ' + item.antibiotic_sensitivity
if table_row:
subject += '<br>' + table_row
if doc.lab_test_comment:
subject += '<br>' + cstr(doc.lab_test_comment)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.result_date
medical_record.reference_doctype = 'Lab Test'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions = True)
def delete_lab_test_from_medical_record(self):
medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name))
if medical_record_id and medical_record_id[0][0]:
frappe.delete_doc('Patient Medical Record', medical_record_id[0][0])
@frappe.whitelist() @frappe.whitelist()
def get_lab_test_prescribed(patient): def get_lab_test_prescribed(patient):

View File

@ -31,12 +31,12 @@ frappe.ui.form.on('Patient Appointment', {
}; };
}); });
frm.set_query('service_unit', function(){ frm.set_query('service_unit', function() {
return { return {
query: 'erpnext.controllers.queries.get_healthcare_service_units',
filters: { filters: {
'is_group': false, company: frm.doc.company,
'allow_appointments': true, inpatient_record: frm.doc.inpatient_record
'company': frm.doc.company
} }
}; };
}); });

View File

@ -18,6 +18,7 @@ from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_pr
class PatientAppointment(Document): class PatientAppointment(Document):
def validate(self): def validate(self):
self.validate_overlaps() self.validate_overlaps()
self.validate_service_unit()
self.set_appointment_datetime() self.set_appointment_datetime()
self.validate_customer_created() self.validate_customer_created()
self.set_status() self.set_status()
@ -68,6 +69,19 @@ class PatientAppointment(Document):
overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4]) overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4])
frappe.throw(overlapping_details, title=_('Appointments Overlapping')) frappe.throw(overlapping_details, title=_('Appointments Overlapping'))
def validate_service_unit(self):
if self.inpatient_record and self.service_unit:
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
is_inpatient_occupancy_unit = frappe.db.get_value('Healthcare Service Unit', self.service_unit,
'inpatient_occupancy')
service_unit = get_current_healthcare_service_unit(self.inpatient_record)
if is_inpatient_occupancy_unit and service_unit != self.service_unit:
msg = _('Patient {0} is not admitted in the service unit {1}').format(frappe.bold(self.patient), frappe.bold(self.service_unit)) + '<br>'
msg += _('Appointment for service units with Inpatient Occupancy can only be created against the unit where patient has been admitted.')
frappe.throw(msg, title=_('Invalid Healthcare Service Unit'))
def set_appointment_datetime(self): def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00") self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter
from frappe.utils import nowdate, add_days from frappe.utils import nowdate, add_days, now_datetime
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
@ -78,6 +78,59 @@ class TestPatientAppointment(unittest.TestCase):
sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled') self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled')
def test_appointment_booking_for_admission_service_unit(self):
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit)
self.assertEqual(appointment.service_unit, service_unit)
# Discharge
schedule_discharge(frappe.as_json({'patient': patient}))
ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1)
def test_invalid_healthcare_service_unit_validation(self):
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment')
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0)
self.assertRaises(frappe.exceptions.ValidationError, appointment.save)
# Discharge
schedule_discharge(frappe.as_json({'patient': patient}))
ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1)
def create_healthcare_docs(): def create_healthcare_docs():
patient = create_patient() patient = create_patient()
@ -125,7 +178,7 @@ def create_encounter(appointment):
encounter.submit() encounter.submit()
return encounter return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0): def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1):
item = create_healthcare_service_items() item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
@ -136,12 +189,15 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.appointment_date = appointment_date appointment.appointment_date = appointment_date
appointment.company = '_Test Company' appointment.company = '_Test Company'
appointment.duration = 15 appointment.duration = 15
if service_unit:
appointment.service_unit = service_unit
if invoice: if invoice:
appointment.mode_of_payment = 'Cash' appointment.mode_of_payment = 'Cash'
appointment.paid_amount = 500 appointment.paid_amount = 500
if procedure_template: if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name') appointment.procedure_template = create_clinical_procedure_template().get('name')
appointment.save(ignore_permissions=True) if save:
appointment.save(ignore_permissions=True)
return appointment return appointment
def create_healthcare_service_items(): def create_healthcare_service_items():
@ -152,6 +208,7 @@ def create_healthcare_service_items():
item.item_name = 'Consulting Charges' item.item_name = 'Consulting Charges'
item.item_group = 'Services' item.item_group = 'Services'
item.is_stock_item = 0 item.is_stock_item = 0
item.stock_uom = 'Nos'
item.save() item.save()
return item.name return item.name

View File

@ -210,7 +210,7 @@
{ {
"fieldname": "drug_prescription", "fieldname": "drug_prescription",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items", "label": "Drug Prescription",
"options": "Drug Prescription" "options": "Drug Prescription"
}, },
{ {
@ -328,7 +328,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-16 21:00:08.644531", "modified": "2020-11-30 10:39:00.783119",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Patient Encounter", "name": "Patient Encounter",

View File

@ -17,10 +17,6 @@ class PatientEncounter(Document):
def on_update(self): def on_update(self):
if self.appointment: if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
update_encounter_medical_record(self)
def after_insert(self):
insert_encounter_to_medical_record(self)
def on_submit(self): def on_submit(self):
if self.therapies: if self.therapies:
@ -33,8 +29,6 @@ class PatientEncounter(Document):
if self.inpatient_record and self.drug_prescription: if self.inpatient_record and self.drug_prescription:
delete_ip_medication_order(self) delete_ip_medication_order(self)
delete_medical_record(self)
def set_title(self): def set_title(self):
self.title = _('{0} with {1}').format(self.patient_name or self.patient, self.title = _('{0} with {1}').format(self.patient_name or self.patient,
self.practitioner_name or self.practitioner)[:100] self.practitioner_name or self.practitioner)[:100]
@ -102,61 +96,7 @@ def create_therapy_plan(encounter):
frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True) frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
def insert_encounter_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.encounter_date
medical_record.reference_doctype = 'Patient Encounter'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)
def update_encounter_medical_record(encounter):
medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name})
if medical_record_id and medical_record_id[0][0]:
subject = set_subject_field(encounter)
frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject)
else:
insert_encounter_to_medical_record(encounter)
def delete_medical_record(encounter):
record = frappe.db.exists('Patient Medical Record', {'reference_name', encounter.name})
if record:
frappe.delete_doc('Patient Medical Record', record, force=1)
def delete_ip_medication_order(encounter): def delete_ip_medication_order(encounter):
record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name}) record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name})
if record: if record:
frappe.delete_doc('Inpatient Medication Order', record, force=1) frappe.delete_doc('Inpatient Medication Order', record, force=1)
def set_subject_field(encounter):
subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '<br>'
if encounter.symptoms:
subject += frappe.bold(_('Symptoms: ')) + '<br>'
for entry in encounter.symptoms:
subject += cstr(entry.complaint) + '<br>'
else:
subject += frappe.bold(_('No Symptoms')) + '<br>'
if encounter.diagnosis:
subject += frappe.bold(_('Diagnosis: ')) + '<br>'
for entry in encounter.diagnosis:
subject += cstr(entry.diagnosis) + '<br>'
else:
subject += frappe.bold(_('No Diagnosis')) + '<br>'
if encounter.drug_prescription:
subject += '<br>' + _('Drug(s) Prescribed.')
if encounter.lab_test_prescription:
subject += '<br>' + _('Test(s) Prescribed.')
if encounter.procedure_prescription:
subject += '<br>' + _('Procedure(s) Prescribed.')
return subject

View File

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2020-11-25 13:40:23.054469",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"date_fieldname",
"add_edit_fields",
"selected_fields"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "selected_fields",
"fieldtype": "Code",
"label": "Selected Fields",
"read_only": 1
},
{
"fieldname": "add_edit_fields",
"fieldtype": "Button",
"in_list_view": 1,
"label": "Add / Edit Fields"
},
{
"fieldname": "date_fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Date Fieldname",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-30 13:54:37.474671",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Custom Document Type",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PatientHistoryCustomDocumentType(Document):
pass

View File

@ -0,0 +1,133 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Patient History Settings', {
refresh: function(frm) {
frm.set_query('document_type', 'custom_doctypes', () => {
return {
filters: {
custom: 1,
is_submittable: 1,
module: 'Healthcare',
}
};
});
},
field_selector: function(frm, doc, standard=1) {
let document_fields = [];
if (doc.selected_fields)
document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname);
frm.call({
method: 'get_doctype_fields',
doc: frm.doc,
args: {
document_type: doc.document_type,
fields: document_fields
},
freeze: true,
callback: function(r) {
if (r.message) {
let doctype = 'Patient History Custom Document Type';
if (standard)
doctype = 'Patient History Standard Document Type';
frm.events.show_field_selector_dialog(frm, doc, doctype, r.message);
}
}
});
},
show_field_selector_dialog: function(frm, doc, doctype, doc_fields) {
let d = new frappe.ui.Dialog({
title: __('{0} Fields', [__(doc.document_type)]),
fields: [
{
label: __('Select Fields'),
fieldtype: 'MultiCheck',
fieldname: 'fields',
options: doc_fields,
columns: 2
}
]
});
d.$body.prepend(`
<div class="columns-search">
<input type="text" placeholder="${__('Search')}" data-element="search" class="form-control input-xs">
</div>`
);
frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area');
d.set_primary_action(__('Save'), () => {
let values = d.get_values().fields;
let selected_fields = [];
frappe.model.with_doctype(doc.document_type, function() {
for (let idx in values) {
let value = values[idx];
let field = frappe.get_meta(doc.document_type).fields.filter((df) => df.fieldname == value)[0];
if (field) {
selected_fields.push({
label: field.label,
fieldname: field.fieldname,
fieldtype: field.fieldtype
});
}
}
d.refresh();
frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields));
});
d.hide();
});
d.show();
},
get_date_field_for_dt: function(frm, row) {
frm.call({
method: 'get_date_field_for_dt',
doc: frm.doc,
args: {
document_type: row.document_type
},
callback: function(data) {
if (data.message) {
frappe.model.set_value('Patient History Custom Document Type',
row.name, 'date_fieldname', data.message);
}
}
});
}
});
frappe.ui.form.on('Patient History Custom Document Type', {
document_type: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.get_date_field_for_dt(frm, row);
}
},
add_edit_fields: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.field_selector(frm, row, 0);
}
}
});
frappe.ui.form.on('Patient History Standard Document Type', {
add_edit_fields: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.field_selector(frm, row);
}
}
});

View File

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2020-11-25 13:41:37.675518",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"standard_doctypes",
"section_break_2",
"custom_doctypes"
],
"fields": [
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "custom_doctypes",
"fieldtype": "Table",
"label": "Custom Document Types",
"options": "Patient History Custom Document Type"
},
{
"fieldname": "standard_doctypes",
"fieldtype": "Table",
"label": "Standard Document Types",
"options": "Patient History Standard Document Type",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-11-25 13:43:38.511771",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.utils import cstr, cint
from frappe.model.document import Document
from erpnext.healthcare.page.patient_history.patient_history import get_patient_history_doctypes
class PatientHistorySettings(Document):
def validate(self):
self.validate_submittable_doctypes()
self.validate_date_fieldnames()
def validate_submittable_doctypes(self):
for entry in self.custom_doctypes:
if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')):
msg = _('Row #{0}: Document Type {1} is not submittable. ').format(
entry.idx, frappe.bold(entry.document_type))
msg += _('Patient Medical Record can only be created for submittable document types.')
frappe.throw(msg)
def validate_date_fieldnames(self):
for entry in self.custom_doctypes:
field = frappe.get_meta(entry.document_type).get_field(entry.date_fieldname)
if not field:
frappe.throw(_('Row #{0}: No such Field named {1} found in the Document Type {2}.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
if field.fieldtype not in ['Date', 'Datetime']:
frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
def get_doctype_fields(self, document_type, fields):
multicheck_fields = []
doc_fields = frappe.get_meta(document_type).fields
for field in doc_fields:
if field.fieldtype not in frappe.model.no_value_fields or \
field.fieldtype in frappe.model.table_fields and not field.hidden:
multicheck_fields.append({
'label': field.label,
'value': field.fieldname,
'checked': 1 if field.fieldname in fields else 0
})
return multicheck_fields
def get_date_field_for_dt(self, document_type):
meta = frappe.get_meta(document_type)
date_fields = meta.get('fields', {
'fieldtype': ['in', ['Date', 'Datetime']]
})
if date_fields:
return date_fields[0].get('fieldname')
def create_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
if frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }):
return
subject = set_subject_field(doc)
date_field = get_date_field(doc.doctype)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.get(date_field)
medical_record.reference_doctype = doc.doctype
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)
def update_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
medical_record_id = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name })
if medical_record_id:
subject = set_subject_field(doc)
frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject)
else:
create_medical_record(doc)
def delete_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
record = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name })
if record:
frappe.delete_doc('Patient Medical Record', record, force=1)
def set_subject_field(doc):
from frappe.utils.formatters import format_value
meta = frappe.get_meta(doc.doctype)
subject = ''
patient_history_fields = get_patient_history_fields(doc)
for entry in patient_history_fields:
fieldname = entry.get('fieldname')
if entry.get('fieldtype') == 'Table' and doc.get(fieldname):
formatted_value = get_formatted_value_for_table_field(doc.get(fieldname), meta.get_field(fieldname))
subject += frappe.bold(_(entry.get('label')) + ': ') + '<br>' + cstr(formatted_value) + '<br>'
else:
if doc.get(fieldname):
formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc)
subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formatted_value) + '<br>'
return subject
def get_date_field(doctype):
dt = get_patient_history_config_dt(doctype)
return frappe.db.get_value(dt, { 'document_type': doctype }, 'date_fieldname')
def get_patient_history_fields(doc):
dt = get_patient_history_config_dt(doc.doctype)
patient_history_fields = frappe.db.get_value(dt, { 'document_type': doc.doctype }, 'selected_fields')
if patient_history_fields:
return json.loads(patient_history_fields)
def get_formatted_value_for_table_field(items, df):
child_meta = frappe.get_meta(df.options)
table_head = ''
table_row = ''
html = ''
create_head = True
for item in items:
table_row += '<tr>'
for cdf in child_meta.fields:
if cdf.in_list_view:
if create_head:
table_head += '<td>' + cdf.label + '</td>'
if item.get(cdf.fieldname):
table_row += '<td>' + str(item.get(cdf.fieldname)) + '</td>'
else:
table_row += '<td></td>'
create_head = False
table_row += '</tr>'
html += "<table class='table table-condensed table-bordered'>" + table_head + table_row + "</table>"
return html
def get_patient_history_config_dt(doctype):
if frappe.db.get_value('DocType', doctype, 'custom'):
return 'Patient History Custom Document Type'
else:
return 'Patient History Standard Document Type'
def validate_medical_record_required(doc):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard \
or get_module(doc) != 'Healthcare':
return False
if doc.doctype not in get_patient_history_doctypes():
return False
return True
def get_module(doc):
module = doc.meta.module
if not module:
module = frappe.db.get_value('DocType', doc.doctype, 'module')
return module

View File

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
import json
from frappe.utils import getdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
class TestPatientHistorySettings(unittest.TestCase):
def setUp(self):
dt = create_custom_doctype()
settings = frappe.get_single("Patient History Settings")
settings.append("custom_doctypes", {
"document_type": dt.name,
"date_fieldname": "date",
"selected_fields": json.dumps([{
"label": "Date",
"fieldname": "date",
"fieldtype": "Date"
},
{
"label": "Rating",
"fieldname": "rating",
"fieldtype": "Rating"
},
{
"label": "Feedback",
"fieldname": "feedback",
"fieldtype": "Small Text"
}])
})
settings.save()
def test_custom_doctype_medical_record(self):
# tests for medical record creation of standard doctypes in test_patient_medical_record.py
patient = create_patient()
doc = create_doc(patient)
# check for medical record
medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name})
self.assertTrue(medical_rec)
medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
expected_subject = "<b>Date: </b>{0}<br><b>Rating: </b>3<br><b>Feedback: </b>Test Patient History Settings<br>".format(
frappe.utils.format_date(getdate()))
self.assertEqual(medical_rec.subject, expected_subject)
self.assertEqual(medical_rec.patient, patient)
self.assertEqual(medical_rec.communication_date, getdate())
def create_custom_doctype():
if not frappe.db.exists("DocType", "Test Patient Feedback"):
doc = frappe.get_doc({
"doctype": "DocType",
"module": "Healthcare",
"custom": 1,
"is_submittable": 1,
"fields": [{
"label": "Date",
"fieldname": "date",
"fieldtype": "Date"
},
{
"label": "Patient",
"fieldname": "patient",
"fieldtype": "Link",
"options": "Patient"
},
{
"label": "Rating",
"fieldname": "rating",
"fieldtype": "Rating"
},
{
"label": "Feedback",
"fieldname": "feedback",
"fieldtype": "Small Text"
}],
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": "Test Patient Feedback",
})
doc.insert()
return doc
else:
return frappe.get_doc("DocType", "Test Patient Feedback")
def create_doc(patient):
doc = frappe.get_doc({
"doctype": "Test Patient Feedback",
"patient": patient,
"date": getdate(),
"rating": 3,
"feedback": "Test Patient History Settings"
}).insert()
doc.submit()
return doc

View File

@ -0,0 +1,57 @@
{
"actions": [],
"creation": "2020-11-25 13:39:36.014814",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"date_fieldname",
"add_edit_fields",
"selected_fields"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "selected_fields",
"fieldtype": "Code",
"label": "Selected Fields",
"read_only": 1
},
{
"fieldname": "add_edit_fields",
"fieldtype": "Button",
"in_list_view": 1,
"label": "Add / Edit Fields"
},
{
"fieldname": "date_fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Date Fieldname",
"read_only": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-30 13:54:56.773325",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Standard Document Type",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PatientHistoryStandardDocumentType(Document):
pass

View File

@ -18,6 +18,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
encounter = create_encounter(appointment) encounter = create_encounter(appointment)
# check for encounter # check for encounter
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': encounter.name}) medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': encounter.name})
self.assertTrue(medical_rec) self.assertTrue(medical_rec)

View File

@ -41,7 +41,6 @@ class TherapySession(Document):
def on_submit(self): def on_submit(self):
self.update_sessions_count_in_therapy_plan() self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self)
def on_update(self): def on_update(self):
if self.appointment: if self.appointment:
@ -142,23 +141,3 @@ def get_therapy_item(therapy, item):
item.reference_dt = 'Therapy Session' item.reference_dt = 'Therapy Session'
item.reference_dn = therapy.name item.reference_dn = therapy.name
return item return item
def insert_session_medical_record(doc):
subject = frappe.bold(_('Therapy: ')) + cstr(doc.therapy_type) + '<br>'
if doc.therapy_plan:
subject += frappe.bold(_('Therapy Plan: ')) + cstr(doc.therapy_plan) + '<br>'
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
subject += frappe.bold(_('Total Counts Targeted: ')) + cstr(doc.total_counts_targeted) + '<br>'
subject += frappe.bold(_('Total Counts Completed: ')) + cstr(doc.total_counts_completed) + '<br>'
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.start_date
medical_record.reference_doctype = 'Therapy Session'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)

View File

@ -12,47 +12,7 @@ class VitalSigns(Document):
def validate(self): def validate(self):
self.set_title() self.set_title()
def on_submit(self):
insert_vital_signs_to_medical_record(self)
def on_cancel(self):
delete_vital_signs_from_medical_record(self)
def set_title(self): def set_title(self):
self.title = _('{0} on {1}').format(self.patient_name or self.patient, self.title = _('{0} on {1}').format(self.patient_name or self.patient,
frappe.utils.format_date(self.signs_date))[:100] frappe.utils.format_date(self.signs_date))[:100]
def insert_vital_signs_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.signs_date
medical_record.reference_doctype = 'Vital Signs'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.flags.ignore_mandatory = True
medical_record.save(ignore_permissions=True)
def delete_vital_signs_from_medical_record(doc):
medical_record = frappe.db.get_value('Patient Medical Record', {'reference_name': doc.name})
if medical_record:
frappe.delete_doc('Patient Medical Record', medical_record)
def set_subject_field(doc):
subject = ''
if doc.temperature:
subject += frappe.bold(_('Temperature: ')) + cstr(doc.temperature) + '<br>'
if doc.pulse:
subject += frappe.bold(_('Pulse: ')) + cstr(doc.pulse) + '<br>'
if doc.respiratory_rate:
subject += frappe.bold(_('Respiratory Rate: ')) + cstr(doc.respiratory_rate) + '<br>'
if doc.bp:
subject += frappe.bold(_('BP: ')) + cstr(doc.bp) + '<br>'
if doc.bmi:
subject += frappe.bold(_('BMI: ')) + cstr(doc.bmi) + '<br>'
if doc.nutrition_note:
subject += frappe.bold(_('Note: ')) + cstr(doc.nutrition_note) + '<br>'
return subject

View File

@ -109,6 +109,11 @@
padding-right: 0px; padding-right: 0px;
} }
.patient-history-filter {
margin-left: 35px;
width: 25%;
}
#page-medical_record .plot-wrapper { #page-medical_record .plot-wrapper {
padding: 20px 15px; padding: 20px 15px;
border-bottom: 1px solid #d1d8dd; border-bottom: 1px solid #d1d8dd;

View File

@ -1,6 +1,5 @@
<div class="col-sm-12"> <div class="col-sm-12">
<div class="col-sm-3"> <div class="col-sm-3">
<p class="text-center">{%= __("Select Patient") %}</p>
<p class="patient" style="margin: auto; max-width: 300px; margin-bottom: 20px;"></p> <p class="patient" style="margin: auto; max-width: 300px; margin-bottom: 20px;"></p>
<div class="patient_details" style="z-index=0"></div> <div class="patient_details" style="z-index=0"></div>
</div> </div>
@ -11,6 +10,13 @@
<div id="chart" class="col-sm-12 patient_vital_charts"> <div id="chart" class="col-sm-12 patient_vital_charts">
</div> </div>
</div> </div>
<div class="header-separator col-sm-12 d-flex border-bottom py-3" style="display:none"></div>
<div class="row">
<div class="col-sm-12 d-flex">
<div class="patient-history-filter doctype-filter"></div>
<div class="patient-history-filter date-filter"></div>
</div>
</div>
<div class="col-sm-12 patient_documents_list"> <div class="col-sm-12 patient_documents_list">
</div> </div>
<div class="col-sm-12 text-center py-3"> <div class="col-sm-12 text-center py-3">

View File

@ -1,141 +1,225 @@
frappe.provide("frappe.patient_history"); frappe.provide('frappe.patient_history');
frappe.pages['patient_history'].on_page_load = function(wrapper) { frappe.pages['patient_history'].on_page_load = function(wrapper) {
var me = this; let me = this;
var page = frappe.ui.make_app_page({ let page = frappe.ui.make_app_page({
parent: wrapper, parent: wrapper,
title: 'Patient History', title: 'Patient History',
single_column: true single_column: true
}); });
frappe.breadcrumbs.add("Healthcare"); frappe.breadcrumbs.add('Healthcare');
let pid = ''; let pid = '';
page.main.html(frappe.render_template("patient_history", {})); page.main.html(frappe.render_template('patient_history', {}));
var patient = frappe.ui.form.make_control({ page.main.find('.header-separator').hide();
parent: page.main.find(".patient"),
let patient = frappe.ui.form.make_control({
parent: page.main.find('.patient'),
df: { df: {
fieldtype: "Link", fieldtype: 'Link',
options: "Patient", options: 'Patient',
fieldname: "patient", fieldname: 'patient',
change: function(){ placeholder: __('Select Patient'),
if(pid != patient.get_value() && patient.get_value()){ only_select: true,
change: function() {
let patient_id = patient.get_value();
if (pid != patient_id && patient_id) {
me.start = 0; me.start = 0;
me.page.main.find(".patient_documents_list").html(""); me.page.main.find('.patient_documents_list').html('');
get_documents(patient.get_value(), me); setup_filters(patient_id, me);
show_patient_info(patient.get_value(), me); get_documents(patient_id, me);
show_patient_vital_charts(patient.get_value(), me, "bp", "mmHg", "Blood Pressure"); show_patient_info(patient_id, me);
show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure');
} }
pid = patient.get_value(); pid = patient_id;
} }
}, },
only_input: true,
}); });
patient.refresh(); patient.refresh();
if (frappe.route_options){ if (frappe.route_options) {
patient.set_value(frappe.route_options.patient); patient.set_value(frappe.route_options.patient);
} }
this.page.main.on("click", ".btn-show-chart", function() { this.page.main.on('click', '.btn-show-chart', function() {
var btn_show_id = $(this).attr("data-show-chart-id"), pts = $(this).attr("data-pts"); let btn_show_id = $(this).attr('data-show-chart-id'), pts = $(this).attr('data-pts');
var title = $(this).attr("data-title"); let title = $(this).attr('data-title');
show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title); show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title);
}); });
this.page.main.on("click", ".btn-more", function() { this.page.main.on('click', '.btn-more', function() {
var doctype = $(this).attr("data-doctype"), docname = $(this).attr("data-docname"); let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname');
if(me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched') == "1"){ if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') {
me.page.main.find("."+docname).hide(); me.page.main.find('.'+docname).hide();
me.page.main.find("."+docname).parent().find('.document-html').show(); me.page.main.find('.'+docname).parent().find('.document-html').show();
}else{ } else {
if(doctype && docname){ if (doctype && docname) {
let exclude = ["patient", "patient_name", 'patient_sex', "encounter_date"]; let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date'];
frappe.call({ frappe.call({
method: "erpnext.healthcare.utils.render_doc_as_html", method: 'erpnext.healthcare.utils.render_doc_as_html',
args:{ args:{
doctype: doctype, doctype: doctype,
docname: docname, docname: docname,
exclude_fields: exclude exclude_fields: exclude
}, },
freeze: true,
callback: function(r) { callback: function(r) {
if (r.message){ if (r.message) {
me.page.main.find("."+docname).hide(); me.page.main.find('.' + docname).hide();
me.page.main.find("."+docname).parent().find('.document-html').html(r.message.html+"\
<div align='center'><a class='btn octicon octicon-chevron-up btn-default btn-xs\ me.page.main.find('.' + docname).parent().find('.document-html').html(
btn-less' data-doctype='"+doctype+"' data-docname='"+docname+"'></a></div>"); `${r.message.html}
me.page.main.find("."+docname).parent().find('.document-html').show(); <div align='center'>
me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched', "1"); <a class='btn octicon octicon-chevron-up btn-default btn-xs btn-less'
data-doctype='${doctype}'
data-docname='${docname}'>
</a>
</div>
`);
me.page.main.find('.' + docname).parent().find('.document-html').show();
me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1');
} }
}, }
freeze: true
}); });
} }
} }
}); });
this.page.main.on("click", ".btn-less", function() { this.page.main.on('click', '.btn-less', function() {
var docname = $(this).attr("data-docname"); let docname = $(this).attr('data-docname');
me.page.main.find("."+docname).parent().find('.document-id').show(); me.page.main.find('.' + docname).parent().find('.document-id').show();
me.page.main.find("."+docname).parent().find('.document-html').hide(); me.page.main.find('.' + docname).parent().find('.document-html').hide();
}); });
me.start = 0; me.start = 0;
me.page.main.on("click", ".btn-get-records", function(){ me.page.main.on('click', '.btn-get-records', function() {
get_documents(patient.get_value(), me); get_documents(patient.get_value(), me);
}); });
}; };
var get_documents = function(patient, me){ let setup_filters = function(patient, me) {
$('.doctype-filter').empty();
frappe.xcall(
'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes'
).then(document_types => {
let doctype_filter = frappe.ui.form.make_control({
parent: $('.doctype-filter'),
df: {
fieldtype: 'MultiSelectList',
fieldname: 'document_type',
placeholder: __('Select Document Type'),
input_class: 'input-xs',
change: () => {
me.start = 0;
me.page.main.find('.patient_documents_list').html('');
get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value());
},
get_data: () => {
return document_types.map(document_type => {
return {
description: document_type,
value: document_type
};
});
},
}
});
doctype_filter.refresh();
$('.date-filter').empty();
let date_range_field = frappe.ui.form.make_control({
df: {
fieldtype: 'DateRange',
fieldname: 'date_range',
placeholder: __('Date Range'),
input_class: 'input-xs',
change: () => {
let selected_date_range = date_range_field.get_value();
if (selected_date_range && selected_date_range.length === 2) {
me.start = 0;
me.page.main.find('.patient_documents_list').html('');
get_documents(patient, me, doctype_filter.get_value(), selected_date_range);
}
}
},
parent: $('.date-filter')
});
date_range_field.refresh();
});
};
let get_documents = function(patient, me, document_types="", selected_date_range="") {
let filters = {
name: patient,
start: me.start,
page_length: 20
};
if (document_types)
filters['document_types'] = document_types;
if (selected_date_range)
filters['date_range'] = selected_date_range;
frappe.call({ frappe.call({
"method": "erpnext.healthcare.page.patient_history.patient_history.get_feed", 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed',
args: { args: filters,
name: patient, callback: function(r) {
start: me.start, let data = r.message;
page_length: 20 if (data.length) {
},
callback: function (r) {
var data = r.message;
if(data.length){
add_to_records(me, data); add_to_records(me, data);
}else{ } else {
me.page.main.find(".patient_documents_list").append("<div class='text-muted' align='center'><br><br>No more records..<br><br></div>"); me.page.main.find('.patient_documents_list').append(`
me.page.main.find(".btn-get-records").hide(); <div class='text-muted' align='center'>
<br><br>${__('No more records..')}<br><br>
</div>`);
me.page.main.find('.btn-get-records').hide();
} }
} }
}); });
}; };
var add_to_records = function(me, data){ let add_to_records = function(me, data) {
var details = "<ul class='nav nav-pills nav-stacked'>"; let details = "<ul class='nav nav-pills nav-stacked'>";
var i; let i;
for(i=0; i<data.length; i++){ for (i=0; i<data.length; i++) {
if(data[i].reference_doctype){ if (data[i].reference_doctype) {
let label = ''; let label = '';
if(data[i].subject){ if (data[i].subject) {
label += "<br/>"+data[i].subject; label += "<br/>" + data[i].subject;
} }
data[i] = add_date_separator(data[i]); data[i] = add_date_separator(data[i]);
if(frappe.user_info(data[i].owner).image){
if (frappe.user_info(data[i].owner).image) {
data[i].imgsrc = frappe.utils.get_file_link(frappe.user_info(data[i].owner).image); data[i].imgsrc = frappe.utils.get_file_link(frappe.user_info(data[i].owner).image);
} } else {
else{
data[i].imgsrc = false; data[i].imgsrc = false;
} }
var time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
time_line_heading += data[i].reference_doctype + " - "+ data[i].reference_name; let time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
details += `<li data-toggle='pill' class='patient_doc_menu' time_line_heading += data[i].reference_doctype + " - " +
data-doctype='${data[i].reference_doctype}' data-docname='${data[i].reference_name}'> `<a onclick="frappe.set_route('Form', '${data[i].reference_doctype}', '${data[i].reference_name}');">
<div class='col-sm-12 d-flex border-bottom py-3'>`; ${data[i].reference_name}
if (data[i].imgsrc){ </a>`;
details += `<span class='mr-3'>
<img class='avtar' src='${data[i].imgsrc}' width='32' height='32'> details += `
</img> <li data-toggle='pill' class='patient_doc_menu'
</span>`; data-doctype='${data[i].reference_doctype}' data-docname='${data[i].reference_name}'>
}else{ <div class='col-sm-12 d-flex border-bottom py-3'>`;
details += `<span class='mr-3 avatar avatar-small' style='width:32px; height:32px;'><div align='center' class='standard-image'
style='background-color: #fafbfc;'>${data[i].practitioner ? data[i].practitioner.charAt(0) : "U"}</div></span>`; if (data[i].imgsrc) {
details += `
<span class='mr-3'>
<img class='avtar' src='${data[i].imgsrc}' width='32' height='32'></img>
</span>`;
} else {
details += `<span class='mr-3 avatar avatar-small' style='width:32px; height:32px;'>
<div align='center' class='standard-image' style='background-color: #fafbfc;'>
${data[i].practitioner ? data[i].practitioner.charAt(0) : 'U'}
</div>
</span>`;
} }
details += `<div class='d-flex flex-column width-full'> details += `<div class='d-flex flex-column width-full'>
<div> <div>
`+time_line_heading+` on `+time_line_heading+`
<span> <span>
${data[i].date_sep} ${data[i].date_sep}
</span> </span>
@ -156,133 +240,150 @@ var add_to_records = function(me, data){
</li>`; </li>`;
} }
} }
details += "</ul>";
me.page.main.find(".patient_documents_list").append(details); details += '</ul>';
me.page.main.find('.patient_documents_list').append(details);
me.start += data.length; me.start += data.length;
if(data.length===20){
if (data.length === 20) {
me.page.main.find(".btn-get-records").show(); me.page.main.find(".btn-get-records").show();
}else{ } else {
me.page.main.find(".btn-get-records").hide(); me.page.main.find(".btn-get-records").hide();
me.page.main.find(".patient_documents_list").append("<div class='text-muted' align='center'><br><br>No more records..<br><br></div>"); me.page.main.find(".patient_documents_list").append(`
<div class='text-muted' align='center'>
<br><br>${__('No more records..')}<br><br>
</div>`);
} }
}; };
var add_date_separator = function(data) { let add_date_separator = function(data) {
var date = frappe.datetime.str_to_obj(data.creation); let date = frappe.datetime.str_to_obj(data.communication_date);
let pdate = '';
let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date));
var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); if (diff < 1) {
if(diff < 1) { pdate = __('Today');
var pdate = 'Today'; } else if (diff < 2) {
} else if(diff < 2) { pdate = __('Yesterday');
pdate = 'Yesterday';
} else { } else {
pdate = frappe.datetime.global_date_format(date); pdate = __('on ') + frappe.datetime.global_date_format(date);
} }
data.date_sep = pdate; data.date_sep = pdate;
return data; return data;
}; };
var show_patient_info = function(patient, me){ let show_patient_info = function(patient, me) {
frappe.call({ frappe.call({
"method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail", 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail',
args: { args: {
patient: patient patient: patient
}, },
callback: function (r) { callback: function(r) {
var data = r.message; let data = r.message;
var details = ""; let details = '';
if(data.image){ if (data.image) {
details += "<div><img class='thumbnail' width=75% src='"+data.image+"'></div>"; details += `<div><img class='thumbnail' width=75% src='${data.image}'></div>`;
} }
details += "<b>" + data.patient_name +"</b><br>" + data.sex;
if(data.email) details += "<br>" + data.email;
if(data.mobile) details += "<br>" + data.mobile;
if(data.occupation) details += "<br><br><b>Occupation :</b> " + data.occupation;
if(data.blood_group) details += "<br><b>Blood group : </b> " + data.blood_group;
if(data.allergies) details += "<br><br><b>Allergies : </b> "+ data.allergies.replace("\n", "<br>");
if(data.medication) details += "<br><b>Medication : </b> "+ data.medication.replace("\n", "<br>");
if(data.alcohol_current_use) details += "<br><br><b>Alcohol use : </b> "+ data.alcohol_current_use;
if(data.alcohol_past_use) details += "<br><b>Alcohol past use : </b> "+ data.alcohol_past_use;
if(data.tobacco_current_use) details += "<br><b>Tobacco use : </b> "+ data.tobacco_current_use;
if(data.tobacco_past_use) details += "<br><b>Tobacco past use : </b> "+ data.tobacco_past_use;
if(data.medical_history) details += "<br><br><b>Medical history : </b> "+ data.medical_history.replace("\n", "<br>");
if(data.surgical_history) details += "<br><b>Surgical history : </b> "+ data.surgical_history.replace("\n", "<br>");
if(data.surrounding_factors) details += "<br><br><b>Occupational hazards : </b> "+ data.surrounding_factors.replace("\n", "<br>");
if(data.other_risk_factors) details += "<br><b>Other risk factors : </b> " + data.other_risk_factors.replace("\n", "<br>");
if(data.patient_details) details += "<br><br><b>More info : </b> " + data.patient_details.replace("\n", "<br>");
if(details){ details += `<b> ${data.patient_name} </b><br> ${data.sex}`;
details = "<div style='padding-left:10px; font-size:13px;' align='center'>" + details + "</div>"; if (data.email) details += `<br> ${data.email}`;
if (data.mobile) details += `<br> ${data.mobile}`;
if (data.occupation) details += `<br><br><b> ${__('Occupation')} : </b> ${data.occupation}`;
if (data.blood_group) details += `<br><b> ${__('Blood Group')} : </b> ${data.blood_group}`;
if (data.allergies) details += `<br><br><b> ${__('Allerigies')} : </b> ${data.allergies.replace("\n", ", ")}`;
if (data.medication) details += `<br><b> ${__('Medication')} : </b> ${data.medication.replace("\n", ", ")}`;
if (data.alcohol_current_use) details += `<br><br><b> ${__('Alcohol use')} : </b> ${data.alcohol_current_use}`;
if (data.alcohol_past_use) details += `<br><b> ${__('Alcohol past use')} : </b> ${data.alcohol_past_use}`;
if (data.tobacco_current_use) details += `<br><b> ${__('Tobacco use')} : </b> ${data.tobacco_current_use}`;
if (data.tobacco_past_use) details += `<br><b> ${__('Tobacco past use')} : </b> ${data.tobacco_past_use}`;
if (data.medical_history) details += `<br><br><b> ${__('Medical history')} : </b> ${data.medical_history.replace("\n", ", ")}`;
if (data.surgical_history) details += `<br><b> ${__('Surgical history')} : </b> ${data.surgical_history.replace("\n", ", ")}`;
if (data.surrounding_factors) details += `<br><br><b> ${__('Occupational hazards')} : </b> ${data.surrounding_factors.replace("\n", ", ")}`;
if (data.other_risk_factors) details += `<br><b> ${__('Other risk factors')} : </b> ${data.other_risk_factors.replace("\n", ", ")}`;
if (data.patient_details) details += `<br><br><b> ${__('More info')} : </b> ${data.patient_details.replace("\n", ", ")}`;
if (details) {
details = `<div style='padding-left:10px; font-size:13px;' align='left'>` + details + `</div>`;
} }
me.page.main.find(".patient_details").html(details); me.page.main.find('.patient_details').html(details);
} }
}); });
}; };
var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
frappe.call({ frappe.call({
method: "erpnext.healthcare.utils.get_patient_vitals", method: 'erpnext.healthcare.utils.get_patient_vitals',
args:{ args:{
patient: patient patient: patient
}, },
callback: function(r) { callback: function(r) {
if (r.message){ if (r.message) {
var show_chart_btns_html = "<div style='padding-top:5px;'><a class='btn btn-default btn-xs btn-show-chart' \ let show_chart_btns_html = `
data-show-chart-id='bp' data-pts='mmHg' data-title='Blood Pressure'>Blood Pressure</a>\ <div style='padding-top:10px;'>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='pulse_rate' \ <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bp' data-pts='mmHg' data-title='Blood Pressure'>
data-pts='per Minutes' data-title='Respiratory/Pulse Rate'>Respiratory/Pulse Rate</a>\ ${__('Blood Pressure')}
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='temperature' \ </a>
data-pts='°C or °F' data-title='Temperature'>Temperature</a>\ <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='pulse_rate' data-pts='per Minutes' data-title='Respiratory/Pulse Rate'>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bmi' \ ${__('Respiratory/Pulse Rate')}
data-pts='' data-title='BMI'>BMI</a></div>"; </a>
me.page.main.find(".show_chart_btns").html(show_chart_btns_html); <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='temperature' data-pts='°C or °F' data-title='Temperature'>
var data = r.message; ${__('Temperature')}
</a>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bmi' data-pts='' data-title='BMI'>
${__('BMI')}
</a>
</div>`;
me.page.main.find('.show_chart_btns').html(show_chart_btns_html);
let data = r.message;
let labels = [], datasets = []; let labels = [], datasets = [];
let bp_systolic = [], bp_diastolic = [], temperature = []; let bp_systolic = [], bp_diastolic = [], temperature = [];
let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = [];
for(var i=0; i<data.length; i++){
labels.push(data[i].signs_date+"||"+data[i].signs_time); for (let i=0; i<data.length; i++) {
if(btn_show_id=="bp"){ labels.push(data[i].signs_date+'||'+data[i].signs_time);
if (btn_show_id === 'bp') {
bp_systolic.push(data[i].bp_systolic); bp_systolic.push(data[i].bp_systolic);
bp_diastolic.push(data[i].bp_diastolic); bp_diastolic.push(data[i].bp_diastolic);
} }
if(btn_show_id=="temperature"){ if (btn_show_id === 'temperature') {
temperature.push(data[i].temperature); temperature.push(data[i].temperature);
} }
if(btn_show_id=="pulse_rate"){ if (btn_show_id === 'pulse_rate') {
pulse.push(data[i].pulse); pulse.push(data[i].pulse);
respiratory_rate.push(data[i].respiratory_rate); respiratory_rate.push(data[i].respiratory_rate);
} }
if(btn_show_id=="bmi"){ if (btn_show_id === 'bmi') {
bmi.push(data[i].bmi); bmi.push(data[i].bmi);
height.push(data[i].height); height.push(data[i].height);
weight.push(data[i].weight); weight.push(data[i].weight);
} }
} }
if(btn_show_id=="temperature"){ if (btn_show_id === 'temperature') {
datasets.push({name: "Temperature", values: temperature, chartType:'line'}); datasets.push({name: 'Temperature', values: temperature, chartType: 'line'});
} }
if(btn_show_id=="bmi"){ if (btn_show_id === 'bmi') {
datasets.push({name: "BMI", values: bmi, chartType:'line'}); datasets.push({name: 'BMI', values: bmi, chartType: 'line'});
datasets.push({name: "Height", values: height, chartType:'line'}); datasets.push({name: 'Height', values: height, chartType: 'line'});
datasets.push({name: "Weight", values: weight, chartType:'line'}); datasets.push({name: 'Weight', values: weight, chartType: 'line'});
} }
if(btn_show_id=="bp"){ if (btn_show_id === 'bp') {
datasets.push({name: "BP Systolic", values: bp_systolic, chartType:'line'}); datasets.push({name: 'BP Systolic', values: bp_systolic, chartType: 'line'});
datasets.push({name: "BP Diastolic", values: bp_diastolic, chartType:'line'}); datasets.push({name: 'BP Diastolic', values: bp_diastolic, chartType: 'line'});
} }
if(btn_show_id=="pulse_rate"){ if (btn_show_id === 'pulse_rate') {
datasets.push({name: "Heart Rate / Pulse", values: pulse, chartType:'line'}); datasets.push({name: 'Heart Rate / Pulse', values: pulse, chartType: 'line'});
datasets.push({name: "Respiratory Rate", values: respiratory_rate, chartType:'line'}); datasets.push({name: 'Respiratory Rate', values: respiratory_rate, chartType: 'line'});
} }
new frappe.Chart( ".patient_vital_charts", { new frappe.Chart('.patient_vital_charts', {
data: { data: {
labels: labels, labels: labels,
datasets: datasets datasets: datasets
}, },
title: title, title: title,
type: 'axis-mixed', // 'axis-mixed', 'bar', 'line', 'pie', 'percentage' type: 'axis-mixed',
height: 200, height: 200,
colors: ['purple', '#ffa3ef', 'light-blue'], colors: ['purple', '#ffa3ef', 'light-blue'],
@ -291,9 +392,11 @@ var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
formatTooltipY: d => d + ' ' + pts, formatTooltipY: d => d + ' ' + pts,
} }
}); });
}else{ me.page.main.find('.header-separator').show();
me.page.main.find(".patient_vital_charts").html(""); } else {
me.page.main.find(".show_chart_btns").html(""); me.page.main.find('.patient_vital_charts').html('');
me.page.main.find('.show_chart_btns').html('');
me.page.main.find('.header-separator').hide();
} }
} }
}); });

View File

@ -4,36 +4,70 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import json
from frappe.utils import cint from frappe.utils import cint
from erpnext.healthcare.utils import render_docs_as_html from erpnext.healthcare.utils import render_docs_as_html
@frappe.whitelist() @frappe.whitelist()
def get_feed(name, start=0, page_length=20): def get_feed(name, document_types=None, date_range=None, start=0, page_length=20):
"""get feed""" """get feed"""
result = frappe.db.sql("""select name, owner, creation, filters = get_filters(name, document_types, date_range)
reference_doctype, reference_name, subject
from `tabPatient Medical Record` result = frappe.db.get_all('Patient Medical Record',
where patient=%(patient)s fields=['name', 'owner', 'communication_date',
order by creation desc 'reference_doctype', 'reference_name', 'subject'],
limit %(start)s, %(page_length)s""", filters=filters,
{ order_by='communication_date DESC',
"patient": name, limit=cint(page_length),
"start": cint(start), start=cint(start)
"page_length": cint(page_length) )
}, as_dict=True)
return result return result
def get_filters(name, document_types=None, date_range=None):
filters = {'patient': name}
if document_types:
document_types = json.loads(document_types)
if len(document_types):
filters['reference_doctype'] = ['IN', document_types]
if date_range:
try:
date_range = json.loads(date_range)
if date_range:
filters['communication_date'] = ['between', [date_range[0], date_range[1]]]
except json.decoder.JSONDecodeError:
pass
return filters
@frappe.whitelist() @frappe.whitelist()
def get_feed_for_dt(doctype, docname): def get_feed_for_dt(doctype, docname):
"""get feed""" """get feed"""
result = frappe.db.sql("""select name, owner, modified, creation, result = frappe.db.get_all('Patient Medical Record',
reference_doctype, reference_name, subject fields=['name', 'owner', 'communication_date',
from `tabPatient Medical Record` 'reference_doctype', 'reference_name', 'subject'],
where reference_name=%(docname)s and reference_doctype=%(doctype)s filters={
order by creation desc""", 'reference_doctype': doctype,
{ 'reference_name': docname
"docname": docname, },
"doctype": doctype order_by='communication_date DESC'
}, as_dict=True) )
return result return result
@frappe.whitelist()
def get_patient_history_doctypes():
document_types = []
settings = frappe.get_single("Patient History Settings")
for entry in settings.standard_doctypes:
document_types.append(entry.document_type)
for entry in settings.custom_doctypes:
document_types.append(entry.document_type)
return document_types

View File

@ -16,6 +16,7 @@ def setup_healthcare():
create_healthcare_item_groups() create_healthcare_item_groups()
create_sensitivity() create_sensitivity()
add_healthcare_service_unit_tree_root() add_healthcare_service_unit_tree_root()
setup_patient_history_settings()
def create_medical_departments(): def create_medical_departments():
departments = [ departments = [
@ -213,3 +214,82 @@ def get_company():
if company: if company:
return company[0].name return company[0].name
return None return None
def setup_patient_history_settings():
import json
settings = frappe.get_single('Patient History Settings')
configuration = get_patient_history_config()
for dt, config in configuration.items():
settings.append("standard_doctypes", {
"document_type": dt,
"date_fieldname": config[0],
"selected_fields": json.dumps(config[1])
})
settings.save()
def get_patient_history_config():
return {
"Patient Encounter": ("encounter_date", [
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Symptoms", "fieldname": "symptoms", "fieldtype": "Table Multiselect"},
{"label": "Diagnosis", "fieldname": "diagnosis", "fieldtype": "Table Multiselect"},
{"label": "Drug Prescription", "fieldname": "drug_prescription", "fieldtype": "Table"},
{"label": "Lab Tests", "fieldname": "lab_test_prescription", "fieldtype": "Table"},
{"label": "Clinical Procedures", "fieldname": "procedure_prescription", "fieldtype": "Table"},
{"label": "Therapies", "fieldname": "therapies", "fieldtype": "Table"},
{"label": "Review Details", "fieldname": "encounter_comment", "fieldtype": "Small Text"}
]),
"Clinical Procedure": ("start_date", [
{"label": "Procedure Template", "fieldname": "procedure_template", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Notes", "fieldname": "notes", "fieldtype": "Small Text"},
{"label": "Service Unit", "fieldname": "service_unit", "fieldtype": "Healthcare Service Unit"},
{"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"},
{"label": "Sample", "fieldname": "sample", "fieldtype": "Link"}
]),
"Lab Test": ("result_date", [
{"label": "Test Template", "fieldname": "template", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Test Name", "fieldname": "lab_test_name", "fieldtype": "Data"},
{"label": "Lab Technician Name", "fieldname": "employee_name", "fieldtype": "Data"},
{"label": "Sample ID", "fieldname": "sample", "fieldtype": "Link"},
{"label": "Normal Test Result", "fieldname": "normal_test_items", "fieldtype": "Table"},
{"label": "Descriptive Test Result", "fieldname": "descriptive_test_items", "fieldtype": "Table"},
{"label": "Organism Test Result", "fieldname": "organism_test_items", "fieldtype": "Table"},
{"label": "Sensitivity Test Result", "fieldname": "sensitivity_test_items", "fieldtype": "Table"},
{"label": "Comments", "fieldname": "lab_test_comment", "fieldtype": "Table"}
]),
"Therapy Session": ("start_date", [
{"label": "Therapy Type", "fieldname": "therapy_type", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Therapy Plan", "fieldname": "therapy_plan", "fieldtype": "Link"},
{"label": "Duration", "fieldname": "duration", "fieldtype": "Int"},
{"label": "Location", "fieldname": "location", "fieldtype": "Link"},
{"label": "Healthcare Service Unit", "fieldname": "service_unit", "fieldtype": "Link"},
{"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"},
{"label": "Exercises", "fieldname": "exercises", "fieldtype": "Table"},
{"label": "Total Counts Targeted", "fieldname": "total_counts_targeted", "fieldtype": "Int"},
{"label": "Total Counts Completed", "fieldname": "total_counts_completed", "fieldtype": "Int"}
]),
"Vital Signs": ("signs_date", [
{"label": "Body Temperature", "fieldname": "temperature", "fieldtype": "Data"},
{"label": "Heart Rate / Pulse", "fieldname": "pulse", "fieldtype": "Data"},
{"label": "Respiratory rate", "fieldname": "respiratory_rate", "fieldtype": "Data"},
{"label": "Tongue", "fieldname": "tongue", "fieldtype": "Select"},
{"label": "Abdomen", "fieldname": "abdomen", "fieldtype": "Select"},
{"label": "Reflexes", "fieldname": "reflexes", "fieldtype": "Select"},
{"label": "Blood Pressure", "fieldname": "bp", "fieldtype": "Data"},
{"label": "Notes", "fieldname": "vital_signs_note", "fieldtype": "Small Text"},
{"label": "Height (In Meter)", "fieldname": "height", "fieldtype": "Float"},
{"label": "Weight (In Kilogram)", "fieldname": "weight", "fieldtype": "Float"},
{"label": "BMI", "fieldname": "bmi", "fieldtype": "Float"}
]),
"Inpatient Medication Order": ("start_date", [
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Start Date", "fieldname": "start_date", "fieldtype": "Date"},
{"label": "End Date", "fieldname": "end_date", "fieldtype": "Date"},
{"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"},
{"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"}
])
}

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import math import math
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded from frappe.utils import time_diff_in_hours, rounded
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
@ -648,11 +649,15 @@ def render_doc_as_html(doctype, docname, exclude_fields = []):
html += "<table class='table table-condensed table-bordered'>" \ html += "<table class='table table-condensed table-bordered'>" \
+ table_head + table_row + "</table>" + table_head + table_row + "</table>"
continue continue
#on other field types add label and value to html #on other field types add label and value to html
if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields: if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields:
html += '<br>{0} : {1}'.format(df.label or df.fieldname, \ if doc.get(df.fieldname):
doc.get(df.fieldname)) formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc)
html += '<br>{0} : {1}'.format(df.label or df.fieldname, formatted_value)
if not has_data : has_data = True if not has_data : has_data = True
if sec_on and col_on and has_data: if sec_on and col_on and has_data:
doc_html += section_html + html + '</div></div>' doc_html += section_html + html + '</div></div>'
elif sec_on and not col_on and has_data: elif sec_on and not col_on and has_data:

View File

@ -221,6 +221,11 @@ standard_queries = {
} }
doc_events = { doc_events = {
"*": {
"on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record",
"on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record",
"on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record"
},
"Stock Entry": { "Stock Entry": {
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty" "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty"
@ -341,7 +346,8 @@ scheduler_events = {
"erpnext.selling.doctype.quotation.quotation.set_expired_status", "erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email" "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
], ],
"daily_long": [ "daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.setup.doctype.email_digest.email_digest.send",

View File

@ -26,6 +26,7 @@ def get_columns(filters):
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
{"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100},
{"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
{"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
@ -43,13 +44,16 @@ def get_data(filters):
for key, qty in iteritems(pledge_values): for key, qty in iteritems(pledge_values):
row = {} row = {}
current_value = flt(qty * loan_security_details.get(key[1])['latest_price']) current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
valid_upto = loan_security_details.get(key[1], {}).get('valid_upto')
row.update(loan_security_details.get(key[1])) row.update(loan_security_details.get(key[1]))
row.update({ row.update({
'applicant_type': applicant_type_map.get(key[0]), 'applicant_type': applicant_type_map.get(key[0]),
'applicant_name': key[0], 'applicant_name': key[0],
'total_qty': qty, 'total_qty': qty,
'current_value': current_value, 'current_value': current_value,
'price_valid_upto': valid_upto,
'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2), 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2),
'currency': currency 'currency': currency
}) })
@ -60,20 +64,30 @@ def get_data(filters):
def get_loan_security_details(filters): def get_loan_security_details(filters):
security_detail_map = {} security_detail_map = {}
loan_security_price_map = {}
lsp_validity_map = {}
loan_security_price_map = frappe._dict(frappe.db.sql(""" loan_security_prices = frappe.db.sql("""
SELECT loan_security, loan_security_price SELECT loan_security, loan_security_price, valid_upto
FROM `tabLoan Security Price` t1 FROM `tabLoan Security Price` t1
WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2 WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2
WHERE t1.loan_security = t2.loan_security) WHERE t1.loan_security = t2.loan_security)
""", as_list=1)) """, as_dict=1)
for security in loan_security_prices:
loan_security_price_map.setdefault(security.loan_security, security.loan_security_price)
lsp_validity_map.setdefault(security.loan_security, security.valid_upto)
loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security', loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security',
'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type', 'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type',
'disabled']) 'disabled'])
for security in loan_security_details: for security in loan_security_details:
security.update({'latest_price': flt(loan_security_price_map.get(security.loan_security))}) security.update({
'latest_price': flt(loan_security_price_map.get(security.loan_security)),
'valid_upto': lsp_validity_map.get(security.loan_security)
})
security_detail_map.setdefault(security.loan_security, security) security_detail_map.setdefault(security.loan_security, security)
return security_detail_map return security_detail_map
@ -118,6 +132,6 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details):
applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0) applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0)
total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \ total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \
* loan_security_details.get(security.loan_security)['latest_price'] * loan_security_details.get(security.loan_security, {}).get('latest_price', 0)
return current_pledges, total_value_map, applicant_type_map return current_pledges, total_value_map, applicant_type_map

View File

@ -6,6 +6,8 @@ import frappe
import erpnext import erpnext
from frappe import _ from frappe import _
from frappe.utils import flt, getdate, add_days from frappe.utils import flt, getdate, add_days
from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
import get_loan_security_details
def execute(filters=None): def execute(filters=None):
@ -31,6 +33,7 @@ def get_columns(filters):
{"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100},
{"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100}, {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100},
{"label": _("Loan To Value Ratio"), "fieldname": "loan_to_value", "fieldtype": "Percent", "width": 100},
{"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
] ]
@ -50,6 +53,9 @@ def get_active_loan_details(filters):
loan_list = [d.loan for d in loan_details] loan_list = [d.loan for d in loan_details]
current_pledges = get_loan_wise_pledges(filters)
loan_wise_security_value = get_loan_wise_security_value(filters, current_pledges)
sanctioned_amount_map = get_sanctioned_amount_map() sanctioned_amount_map = get_sanctioned_amount_map()
penal_interest_rate_map = get_penal_interest_rate_map() penal_interest_rate_map = get_penal_interest_rate_map()
payments = get_payments(loan_list) payments = get_payments(loan_list)
@ -67,12 +73,16 @@ def get_active_loan_details(filters):
"penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
"penalty_interest": penal_interest_rate_map.get(loan.loan_type), "penalty_interest": penal_interest_rate_map.get(loan.loan_type),
"undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")), "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")),
"loan_to_value": 0.0,
"currency": currency "currency": currency
}) })
loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \ loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \
+ loan['penalty'] + loan['penalty']
if loan_wise_security_value.get(loan.loan):
loan['loan_to_value'] = (loan['principal_outstanding'] * 100) / loan_wise_security_value.get(loan.loan)
return loan_details return loan_details
def get_sanctioned_amount_map(): def get_sanctioned_amount_map():
@ -121,4 +131,53 @@ def get_interest_accruals(loans):
return accrual_map return accrual_map
def get_penal_interest_rate_map(): def get_penal_interest_rate_map():
return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1)) return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1))
def get_loan_wise_pledges(filters):
loan_wise_unpledges = {}
current_pledges = {}
conditions = ""
if filters.get('company'):
conditions = "AND company = %(company)s"
unpledges = frappe.db.sql("""
SELECT up.loan, u.loan_security, sum(u.qty) as qty
FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
WHERE u.parent = up.name
AND up.status = 'Approved'
{conditions}
GROUP BY up.loan
""".format(conditions=conditions), filters, as_dict=1)
for unpledge in unpledges:
loan_wise_unpledges.setdefault((unpledge.loan, unpledge.loan_security), unpledge.qty)
pledges = frappe.db.sql("""
SELECT lp.loan, p.loan_security, sum(p.qty) as qty
FROM `tabLoan Security Pledge` lp, `tabPledge`p
WHERE p.parent = lp.name
AND lp.status = 'Pledged'
{conditions}
GROUP BY lp.loan
""".format(conditions=conditions), filters, as_dict=1)
for security in pledges:
current_pledges.setdefault((security.loan, security.loan_security), security.qty)
current_pledges[(security.loan, security.loan_security)] -= \
loan_wise_unpledges.get((security.loan, security.loan_security), 0.0)
return current_pledges
def get_loan_wise_security_value(filters, current_pledges):
loan_security_details = get_loan_security_details(filters)
loan_wise_security_value = {}
for key in current_pledges:
qty = current_pledges.get(key)
loan_wise_security_value.setdefault(key[0], 0.0)
loan_wise_security_value[key[0]] += \
flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
return loan_wise_security_value

View File

@ -24,6 +24,7 @@ def get_columns(filters):
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
{"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100},
{"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
{"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100}, {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100},
@ -40,13 +41,16 @@ def get_data(filters):
for security, value in iteritems(current_pledges): for security, value in iteritems(current_pledges):
row = {} row = {}
current_value = flt(value['qty'] * loan_security_details.get(security)['latest_price']) current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0))
valid_upto = loan_security_details.get(security, {}).get('valid_upto')
row.update(loan_security_details.get(security)) row.update(loan_security_details.get(security))
row.update({ row.update({
'total_qty': value['qty'], 'total_qty': value.get('qty'),
'current_value': current_value, 'current_value': current_value,
'price_valid_upto': valid_upto,
'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
'pledged_applicant_count': value['applicant_count'], 'pledged_applicant_count': value.get('applicant_count'),
'currency': currency 'currency': currency
}) })

View File

@ -12,7 +12,6 @@
"membership_expiry_date", "membership_expiry_date",
"column_break_5", "column_break_5",
"membership_type", "membership_type",
"email",
"email_id", "email_id",
"image", "image",
"customer_section", "customer_section",
@ -64,13 +63,6 @@
"options": "Membership Type", "options": "Membership Type",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "email",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User"
},
{ {
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
@ -178,7 +170,7 @@
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-09-16 23:44:13.596948", "modified": "2020-11-09 12:12:10.174647",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Member", "name": "Member",

View File

@ -18,8 +18,6 @@ class Member(Document):
def validate(self): def validate(self):
if self.email:
self.validate_email_type(self.email)
if self.email_id: if self.email_id:
self.validate_email_type(self.email_id) self.validate_email_type(self.email_id)
@ -57,14 +55,16 @@ class Member(Document):
def make_customer_and_link(self): def make_customer_and_link(self):
if self.customer: if self.customer:
frappe.msgprint(_("A customer is already linked to this Member")) frappe.msgprint(_("A customer is already linked to this Member"))
cust = create_customer(frappe._dict({
customer = create_customer(frappe._dict({
'fullname': self.member_name, 'fullname': self.member_name,
'email': self.email_id or self.email, 'email': self.email_id,
'phone': None 'phone': None
})) }))
self.customer = cust self.customer = customer
self.save() self.save()
frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer))
def get_or_create_member(user_details): def get_or_create_member(user_details):

View File

@ -4,16 +4,25 @@
frappe.ui.form.on('Membership', { frappe.ui.form.on('Membership', {
setup: function(frm) { setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
if (val) frm.set_df_property('razorpay_details_section', 'hidden', false); if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
}) })
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.__islocal)
return;
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
frm.call("generate_invoice", { frm.call({
save: true doc: frm.doc,
}).then(() => { method: "generate_invoice",
frm.reload_doc(); args: {save: true},
freeze: true,
freeze_message: __("Creating Membership Invoice"),
callback: function(r) {
if (r.invoice)
frm.reload_doc();
}
}); });
}); });
@ -27,6 +36,6 @@ frappe.ui.form.on('Membership', {
}, },
onload: function(frm) { onload: function(frm) {
frm.add_fetch('membership_type', 'amount', 'amount'); frm.add_fetch("membership_type", "amount", "amount");
} }
}); });

View File

@ -7,6 +7,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"member", "member",
"member_name",
"membership_type", "membership_type",
"column_break_3", "column_break_3",
"membership_status", "membership_status",
@ -46,6 +47,8 @@
{ {
"fieldname": "membership_status", "fieldname": "membership_status",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Membership Status", "label": "Membership Status",
"options": "New\nCurrent\nExpired\nPending\nCancelled" "options": "New\nCurrent\nExpired\nPending\nCancelled"
}, },
@ -122,11 +125,18 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Invoice", "label": "Invoice",
"options": "Sales Invoice" "options": "Sales Invoice"
},
{
"fetch_from": "member.member_name",
"fieldname": "member_name",
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-09-19 14:28:11.532696", "modified": "2021-01-21 16:31:20.032656",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership", "name": "Membership",
@ -158,7 +168,9 @@
} }
], ],
"restrict_to_domain": "Non Profit", "restrict_to_domain": "Non Profit",
"search_fields": "member, member_name",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "member_name",
"track_changes": 1 "track_changes": 1
} }

View File

@ -14,33 +14,43 @@ from erpnext.non_profit.doctype.member.member import create_member
from frappe import _ from frappe import _
import erpnext import erpnext
class Membership(Document): class Membership(Document):
def validate(self): def validate(self):
if not self.member or not frappe.db.exists("Member", self.member): if not self.member or not frappe.db.exists("Member", self.member):
member_name = frappe.get_value('Member', dict(email=frappe.session.user)) # for web forms
user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
if user_type == "Website User":
self.create_member_from_website_user()
else:
frappe.throw(_("Please select a Member"))
if not member_name: self.validate_membership_period()
user = frappe.get_doc('User', frappe.session.user)
member = frappe.get_doc(dict(
doctype='Member',
email=frappe.session.user,
membership_type=self.membership_type,
member_name=user.get_fullname()
)).insert(ignore_permissions=True)
member_name = member.name
if self.get("__islocal"): def create_member_from_website_user(self):
self.member = member_name member_name = frappe.get_value("Member", dict(email_id=frappe.session.user))
if not member_name:
user = frappe.get_doc("User", frappe.session.user)
member = frappe.get_doc(dict(
doctype="Member",
email_id=frappe.session.user,
membership_type=self.membership_type,
member_name=user.get_fullname()
)).insert(ignore_permissions=True)
member_name = member.name
if self.get("__islocal"):
self.member = member_name
def validate_membership_period(self):
# get last membership (if active) # get last membership (if active)
last_membership = erpnext.get_last_membership() last_membership = erpnext.get_last_membership(self.member)
# if person applied for offline membership # if person applied for offline membership
if last_membership and not frappe.session.user == "Administrator": if last_membership and not frappe.session.user == "Administrator":
# if last membership does not expire in 30 days, then do not allow to renew # if last membership does not expire in 30 days, then do not allow to renew
if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
frappe.throw(_('You can only renew if your membership expires within 30 days')) frappe.throw(_("You can only renew if your membership expires within 30 days"))
self.from_date = add_days(last_membership.to_date, 1) self.from_date = add_days(last_membership.to_date, 1)
elif frappe.session.user == "Administrator": elif frappe.session.user == "Administrator":
@ -54,11 +64,16 @@ class Membership(Document):
self.to_date = add_months(self.from_date, 1) self.to_date = add_months(self.from_date, 1)
def on_payment_authorized(self, status_changed_to=None): def on_payment_authorized(self, status_changed_to=None):
if status_changed_to in ("Completed", "Authorized"): if status_changed_to not in ("Completed", "Authorized"):
self.load_from_db() return
self.db_set('paid', 1) self.load_from_db()
self.db_set("paid", 1)
settings = frappe.get_doc("Membership Settings")
if settings.enable_invoicing and settings.create_for_web_forms:
self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True)
def generate_invoice(self, save=True):
def generate_invoice(self, save=True, with_payment_entry=False):
if not (self.paid or self.currency or self.amount): if not (self.paid or self.currency or self.amount):
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
@ -66,34 +81,64 @@ class Membership(Document):
frappe.throw(_("An invoice is already linked to this document")) frappe.throw(_("An invoice is already linked to this document"))
member = frappe.get_doc("Member", self.member) member = frappe.get_doc("Member", self.member)
plan = frappe.get_doc("Membership Type", self.membership_type)
settings = frappe.get_doc("Membership Settings")
if not member.customer: if not member.customer:
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
if not settings.debit_account: plan = frappe.get_doc("Membership Type", self.membership_type)
frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings")) settings = frappe.get_doc("Membership Settings")
self.validate_membership_type_and_settings(plan, settings)
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
invoice = make_invoice(self, member, plan, settings) invoice = make_invoice(self, member, plan, settings)
self.invoice = invoice.name self.invoice = invoice.name
if with_payment_entry:
self.make_payment_entry(settings, invoice)
if save: if save:
self.save() self.save()
return invoice return invoice
def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type)
if not settings.debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in {0}").format(settings_link))
if not plan.linked_item:
frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format(
get_link_to_form("Membership Type", self.membership_type)))
def make_payment_entry(self, settings, invoice):
if not settings.payment_account:
frappe.throw(_("You need to set <b>Payment Account</b> in {0}").format(
get_link_to_form("Membership Type", self.membership_type)))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
frappe.flags.ignore_account_permission=False
pe.paid_to = settings.payment_account
pe.reference_no = self.name
pe.reference_date = getdate()
pe.save(ignore_permissions=True)
pe.submit()
def send_acknowlement(self): def send_acknowlement(self):
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Membership Settings")
if not settings.send_email: if not settings.send_email:
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in Membership Settings")) frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format(
get_link_to_form("Membership Settings", "Membership Settings")))
member = frappe.get_doc("Member", self.member) member = frappe.get_doc("Member", self.member)
if not member.email_id:
frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member)))
plan = frappe.get_doc("Membership Type", self.membership_type) plan = frappe.get_doc("Membership Type", self.membership_type)
email = member.email_id if member.email_id else member.email email = member.email_id
attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
if self.invoice and settings.send_invoice: if self.invoice and settings.send_invoice:
@ -112,48 +157,56 @@ class Membership(Document):
} }
if not frappe.flags.in_test: if not frappe.flags.in_test:
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
else: else:
frappe.sendmail(**email_args) frappe.sendmail(**email_args)
def generate_and_send_invoice(self): def generate_and_send_invoice(self):
invoice = self.generate_invoice(False) self.generate_invoice(save=False)
self.send_acknowlement() self.send_acknowlement()
def make_invoice(membership, member, plan, settings): def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({ invoice = frappe.get_doc({
'doctype': 'Sales Invoice', "doctype": "Sales Invoice",
'customer': member.customer, "customer": member.customer,
'debit_to': settings.debit_account, "debit_to": settings.debit_account,
'currency': membership.currency, "currency": membership.currency,
'is_pos': 0, "company": settings.company,
'items': [ "is_pos": 0,
"items": [
{ {
'item_code': plan.linked_item, "item_code": plan.linked_item,
'rate': membership.amount, "rate": membership.amount,
'qty': 1 "qty": 1
} }
] ]
}) })
invoice.set_missing_values()
invoice.insert(ignore_permissions=True) invoice.insert(ignore_permissions=True)
invoice.submit() invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully"))
return invoice return invoice
def get_member_based_on_subscription(subscription_id, email): def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={ members = frappe.get_all("Member", filters={
'subscription_id': subscription_id, "subscription_id": subscription_id,
'email_id': email "email_id": email
}, order_by="creation desc") }, order_by="creation desc")
try: try:
return frappe.get_doc("Member", members[0]['name']) return frappe.get_doc("Member", members[0]["name"])
except: except:
return None return None
def verify_signature(data): def verify_signature(data):
signature = frappe.request.headers.get('X-Razorpay-Signature') if frappe.flags.in_test:
return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Membership Settings")
key = settings.get_webhook_secret() key = settings.get_webhook_secret()
@ -162,6 +215,7 @@ def verify_signature(data):
controller.verify_signature(data, signature, key) controller.verify_signature(data, signature, key)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs): def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True) data = frappe.request.get_data(as_text=True)
@ -170,16 +224,16 @@ def trigger_razorpay_subscription(*args, **kwargs):
except Exception as e: except Exception as e:
log = frappe.log_error(e, "Webhook Verification Error") log = frappe.log_error(e, "Webhook Verification Error")
notify_failure(log) notify_failure(log)
return { 'status': 'Failed', 'reason': e} return { "status": "Failed", "reason": e}
if isinstance(data, six.string_types): if isinstance(data, six.string_types):
data = json.loads(data) data = json.loads(data)
data = frappe._dict(data) data = frappe._dict(data)
subscription = data.payload.get("subscription", {}).get('entity', {}) subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription) subscription = frappe._dict(subscription)
payment = data.payload.get("payment", {}).get('entity', {}) payment = data.payload.get("payment", {}).get("entity", {})
payment = frappe._dict(payment) payment = frappe._dict(payment)
try: try:
@ -189,15 +243,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member = get_member_based_on_subscription(subscription.id, payment.email) member = get_member_based_on_subscription(subscription.id, payment.email)
if not member: if not member:
member = create_member(frappe._dict({ member = create_member(frappe._dict({
'fullname': payment.email, "fullname": payment.email,
'email': payment.email, "email": payment.email,
'plan_id': get_plan_from_razorpay_id(subscription.plan_id) "plan_id": get_plan_from_razorpay_id(subscription.plan_id)
})) }))
member.subscription_id = subscription.id member.subscription_id = subscription.id
member.customer_id = payment.customer_id member.customer_id = payment.customer_id
if subscription.notes and type(subscription.notes) == dict: if subscription.notes and type(subscription.notes) == dict:
notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
member.add_comment("Comment", notes) member.add_comment("Comment", notes)
elif subscription.notes and type(subscription.notes) == str: elif subscription.notes and type(subscription.notes) == str:
member.add_comment("Comment", subscription.notes) member.add_comment("Comment", subscription.notes)
@ -227,28 +281,39 @@ def trigger_razorpay_subscription(*args, **kwargs):
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log) notify_failure(log)
return { 'status': 'Failed', 'reason': e} return { "status": "Failed", "reason": e}
return { 'status': 'Success' } return { "status": "Success" }
def notify_failure(log): def notify_failure(log):
try: try:
content = """Dear System Manager, content = """
Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below Dear System Manager,
Razorpay webhook for creating renewing membership subscription failed due to some reason.
Please check the following error log linked below
Error Log: {0}
Regards, Administrator
""".format(get_link_to_form("Error Log", log.name))
Error Log: {0}
Regards,
Administrator""".format(get_link_to_form("Error Log", log.name))
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
except: except:
pass pass
def get_plan_from_razorpay_id(plan_id): def get_plan_from_razorpay_id(plan_id):
plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc")
try: try:
return plan[0]['name'] return plan[0]["name"]
except: except:
return None return None
def set_expired_status():
frappe.db.sql("""
UPDATE
`tabMembership` SET `status` = 'Expired'
WHERE
`status` not in ('Cancelled') AND `to_date` < %s
""", (nowdate()))

View File

@ -0,0 +1,15 @@
frappe.listview_settings['Membership'] = {
get_indicator: function(doc) {
if (doc.membership_status == 'New') {
return [__('New'), 'blue', 'membership_status,=,New'];
} else if (doc.membership_status === 'Current') {
return [__('Current'), 'green', 'membership_status,=,Current'];
} else if (doc.membership_status === 'Pending') {
return [__('Pending'), 'yellow', 'membership_status,=,Pending'];
} else if (doc.membership_status === 'Expired') {
return [__('Expired'), 'grey', 'membership_status,=,Expired'];
} else {
return [__('Cancelled'), 'red', 'membership_status,=,Cancelled'];
}
}
};

View File

@ -2,8 +2,110 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe
import erpnext
from erpnext.non_profit.doctype.member.member import create_member
from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase): class TestMembership(unittest.TestCase):
pass def setUp(self):
# Get default company
company = frappe.get_doc("Company", erpnext.get_default_company())
# update membership settings
settings = frappe.get_doc("Membership Settings")
# Enable razorpay
settings.enable_razorpay = 1
settings.billing_cycle = "Monthly"
settings.billing_frequency = 24
# Enable invoicing
settings.enable_invoicing = 1
settings.make_payment_entry = 1
settings.company = company.name
settings.payment_account = company.default_cash_account
settings.debit_account = company.default_receivable_account
settings.save()
# make test plan
if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
plan = frappe.new_doc("Membership Type")
plan.membership_type = "_rzpy_test_milythm"
plan.amount = 100
plan.razorpay_plan_id = "_rzpy_test_milythm"
plan.linked_item = create_item("_Test Item for Non Profit Membership").name
plan.insert()
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
# make test member
self.member_doc = create_member(frappe._dict({
'fullname': "_Test_Member",
'email': "_test_member_erpnext@example.com",
'plan_id': plan.name
}))
self.member_doc.make_customer_and_link()
self.member = self.member_doc.name
def test_auto_generate_invoice_and_payment_entry(self):
entry = make_membership(self.member)
# Naive test to see if at all invoice was generated and attached to member
# In any case if details were missing, the invoicing would throw an error
invoice = entry.generate_invoice(save=True)
self.assertEqual(invoice.name, entry.invoice)
def test_renew_within_30_days(self):
# create a membership for two months
# Should work fine
make_membership(self.member, { "from_date": nowdate() })
make_membership(self.member, { "from_date": add_months(nowdate(), 1) })
from frappe.utils.user import add_role
add_role("test@example.com", "Non Profit Manager")
frappe.set_user("test@example.com")
# create next membership with expiry not within 30 days
self.assertRaises(frappe.ValidationError, make_membership, self.member, {
"from_date": add_months(nowdate(), 2),
})
frappe.set_user("Administrator")
# create the same membership but as administrator
make_membership(self.member, {
"from_date": add_months(nowdate(), 2),
"to_date": add_months(nowdate(), 3),
})
def set_config(key, value):
frappe.db.set_value("Membership Settings", None, key, value)
def make_membership(member, payload={}):
data = {
"doctype": "Membership",
"member": member,
"membership_status": "Current",
"membership_type": "_rzpy_test_milythm",
"currency": "INR",
"paid": 1,
"from_date": nowdate(),
"amount": 100
}
data.update(payload)
membership = frappe.get_doc(data)
membership.insert(ignore_permissions=True, ignore_if_duplicate=True)
return membership
def create_item(item_code):
if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
item.item_name = item_code
item.stock_uom = "Nos"
item.description = item_code
item.item_group = "All Item Groups"
item.is_stock_item = 0
item.save()
else:
item = frappe.get_doc("Item", item_code)
return item

View File

@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", {
}); });
} }
frm.set_query('inv_print_format', function(doc) { frm.set_query("inv_print_format", function() {
return { return {
filters: { filters: {
"doc_type": "Sales Invoice" "doc_type": "Sales Invoice"
@ -19,7 +19,7 @@ frappe.ui.form.on("Membership Settings", {
}; };
}); });
frm.set_query('membership_print_format', function(doc) { frm.set_query("membership_print_format", function() {
return { return {
filters: { filters: {
"doc_type": "Membership" "doc_type": "Membership"
@ -27,12 +27,23 @@ frappe.ui.form.on("Membership Settings", {
}; };
}); });
frm.set_query('debit_account', function(doc) { frm.set_query("debit_account", function() {
return { return {
filters: { filters: {
'account_type': 'Receivable', "account_type": "Receivable",
'is_group': 0, "is_group": 0,
'company': frm.doc.company "company": frm.doc.company
}
};
});
frm.set_query("payment_account", function () {
var account_types = ["Bank", "Cash"];
return {
filters: {
"account_type": ["in", account_types],
"is_group": 0,
"company": frm.doc.company
} }
}; };
}); });

View File

@ -11,9 +11,12 @@
"billing_frequency", "billing_frequency",
"webhook_secret", "webhook_secret",
"column_break_6", "column_break_6",
"enable_auto_invoicing", "enable_invoicing",
"create_for_web_forms",
"make_payment_entry",
"company", "company",
"debit_account", "debit_account",
"payment_account",
"column_break_9", "column_break_9",
"send_email", "send_email",
"send_invoice", "send_invoice",
@ -58,14 +61,7 @@
"label": "Invoicing" "label": "Invoicing"
}, },
{ {
"default": "0", "depends_on": "eval:doc.enable_invoicing",
"fieldname": "enable_auto_invoicing",
"fieldtype": "Check",
"label": "Enable Auto Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"fieldname": "debit_account", "fieldname": "debit_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Debit Account", "label": "Debit Account",
@ -77,7 +73,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval:doc.enable_auto_invoicing", "depends_on": "eval:doc.enable_invoicing",
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
@ -86,7 +82,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", "depends_on": "eval:doc.enable_invoicing && doc.send_email",
"fieldname": "send_invoice", "fieldname": "send_invoice",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Send Invoice with Email" "label": "Send Invoice with Email"
@ -119,11 +115,43 @@
"label": "Email Template", "label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email", "mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template" "options": "Email Template"
},
{
"default": "0",
"fieldname": "enable_invoicing",
"fieldtype": "Check",
"label": "Enable Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
"fieldname": "make_payment_entry",
"fieldtype": "Check",
"label": "Make Payment Entry"
},
{
"depends_on": "eval:doc.make_payment_entry",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment To",
"mandatory_depends_on": "eval:doc.make_payment_entry",
"options": "Account"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Automatically create an invoice when payment is authorized from a web form entry",
"fieldname": "create_for_web_forms",
"fieldtype": "Check",
"label": "Auto Create Invoice for Web Forms"
} }
], ],
"index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-08-05 17:26:37.287395", "modified": "2021-01-21 19:57:53.213286",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership Settings", "name": "Membership Settings",

View File

@ -2,13 +2,21 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Membership Type', { frappe.ui.form.on('Membership Type', {
refresh: function(frm) { refresh: function (frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
}); });
frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false); if (val) frm.set_df_property('linked_item', 'hidden', false);
}); });
frm.set_query('linked_item', () => {
return {
filters: {
is_stock_item: 0
}
};
});
} }
}); });

View File

@ -5,9 +5,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.model.document import Document from frappe.model.document import Document
import frappe import frappe
from frappe import _
class MembershipType(Document): class MembershipType(Document):
pass def validate(self):
if self.linked_item:
is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item")
if is_stock_item:
frappe.throw(_("The Linked Item should be a service item"))
def get_membership_type(razorpay_id): def get_membership_type(razorpay_id):
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})

View File

@ -736,8 +736,9 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_reason_for_resignation_in_employee
erpnext.patches.v13_0.update_custom_fields_for_shopify
execute:frappe.delete_doc("Report", "Quoted Item Comparison") execute:frappe.delete_doc("Report", "Quoted Item Comparison")
erpnext.patches.v13_0.update_member_email_address
erpnext.patches.v13_0.update_custom_fields_for_shopify
erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.add_po_to_global_search
@ -745,3 +746,4 @@ erpnext.patches.v13_0.update_returned_qty_in_pr_dn
erpnext.patches.v13_0.update_project_template_tasks erpnext.patches.v13_0.update_project_template_tasks
erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.set_company_in_leave_ledger_entry
erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.convert_qi_parameter_to_link_field
erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
import frappe
from erpnext.healthcare.setup import setup_patient_history_settings
def execute():
if "Healthcare" not in frappe.get_active_domains():
return
frappe.reload_doc("healthcare", "doctype", "Patient History Settings")
frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type")
frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type")
setup_patient_history_settings()

View File

@ -0,0 +1,23 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
"""add value to email_id column from email"""
if frappe.db.has_column("Member", "email"):
# Get all members
for member in frappe.db.get_all("Member", pluck="name"):
# Check if email_id already exists
if not frappe.db.get_value("Member", member, "email_id"):
# fetch email id from the user linked field email
email = frappe.db.get_value("Member", member, "email")
# Set the value for it
frappe.db.set_value("Member", member, "email_id", email)
if frappe.db.exists("DocType", "Membership Settings"):
rename_field("Membership Settings", "enable_auto_invoicing", "enable_invoicing")

View File

@ -5,40 +5,43 @@ from __future__ import unicode_literals
import frappe import frappe
def execute(): def execute():
frappe.reload_doc("projects", "doctype", "project_template") frappe.reload_doc("projects", "doctype", "project_template")
frappe.reload_doc("projects", "doctype", "project_template_task") frappe.reload_doc("projects", "doctype", "project_template_task")
frappe.reload_doc("projects", "doctype", "project_template") frappe.reload_doc("projects", "doctype", "task")
frappe.reload_doc("projects", "doctype", "task")
for template_name in frappe.db.sql(""" # Update property setter status if any
select property_setter = frappe.db.get_value('Property Setter', {'doc_type': 'Task',
name 'field_name': 'status', 'property': 'options'})
from
`tabProject Template` """,
as_dict=1):
template = frappe.get_doc("Project Template", template_name.name)
replace_tasks = False
new_tasks = []
for task in template.tasks:
if task.subject:
replace_tasks = True
new_task = frappe.get_doc(dict(
doctype = "Task",
subject = task.subject,
start = task.start,
duration = task.duration,
task_weight = task.task_weight,
description = task.description,
is_template = 1
)).insert()
new_tasks.append(new_task)
if replace_tasks: if property_setter:
template.tasks = [] property_setter_doc = frappe.get_doc('Property Setter', {'doc_type': 'Task',
for tsk in new_tasks: 'field_name': 'status', 'property': 'options'})
template.append("tasks", { property_setter_doc.value += "\nTemplate"
"task": tsk.name, property_setter_doc.save()
"subject": tsk.subject
}) for template_name in frappe.get_all('Project Template'):
template.save() template = frappe.get_doc("Project Template", template_name.name)
replace_tasks = False
new_tasks = []
for task in template.tasks:
if task.subject:
replace_tasks = True
new_task = frappe.get_doc(dict(
doctype = "Task",
subject = task.subject,
start = task.start,
duration = task.duration,
task_weight = task.task_weight,
description = task.description,
is_template = 1
)).insert()
new_tasks.append(new_task)
if replace_tasks:
template.tasks = []
for tsk in new_tasks:
template.append("tasks", {
"task": tsk.name,
"subject": tsk.subject
})
template.save()

View File

@ -12,6 +12,8 @@ frappe.ui.form.on('Additional Salary', {
} }
}; };
}); });
frm.trigger('set_earning_component');
}, },
employee: function(frm) { employee: function(frm) {
@ -43,6 +45,20 @@ frappe.ui.form.on('Additional Salary', {
}); });
}, },
company: function(frm) {
frm.trigger('set_earning_component');
},
set_earning_component: function(frm) {
if (!frm.doc.company) return;
frm.set_query("salary_component", function() {
return {
query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {type: "earning", company: frm.doc.company}
};
});
},
get_employee_currency: function(frm) { get_employee_currency: function(frm) {
frappe.call({ frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",

View File

@ -70,6 +70,9 @@ frappe.ui.form.on('Salary Structure', {
}); });
}, },
company: function(frm) {
frm.trigger('set_earning_deduction_component');
},
currency: function(frm) { currency: function(frm) {
calculate_totals(frm.doc); calculate_totals(frm.doc);
@ -117,6 +120,7 @@ frappe.ui.form.on('Salary Structure', {
fields_read_only.forEach(function(field) { fields_read_only.forEach(function(field) {
frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1; frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1;
}); });
frm.trigger('set_earning_deduction_component');
}, },
assign_to_employees:function (frm) { assign_to_employees:function (frm) {

View File

@ -216,8 +216,13 @@ def get_earning_deduction_components(doctype, txt, searchfield, start, page_len,
return frappe.db.sql(""" return frappe.db.sql("""
select t1.salary_component select t1.salary_component
from `tabSalary Component` t1, `tabSalary Component Account` t2 from `tabSalary Component` t1, `tabSalary Component Account` t2
where t1.salary_component = t2.parent where (t1.name = t2.parent
and t1.type = %s and t1.type = %(type)s
and t2.company = %s and t2.company = %(company)s)
or (t1.type = %(type)s
and t1.statistical_component = 1)
order by salary_component order by salary_component
""", (filters['type'], filters['company']) ) """,{
"type": filters['type'],
"company": filters['company']
})

View File

@ -18,6 +18,9 @@ erpnext.setup_einvoice_actions = (doctype) => {
if (!irn && !__unsaved) { if (!irn && !__unsaved) {
const action = () => { const action = () => {
if (frm.doc.__unsaved) {
frappe.throw(__('Please save the document to generate IRN.'));
}
frappe.call({ frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.get_einvoice', method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
args: { doctype, docname: name }, args: { doctype, docname: name },

View File

@ -11,6 +11,7 @@ import json
import base64 import base64
import frappe import frappe
import traceback import traceback
import io
from frappe import _, bold from frappe import _, bold
from pyqrcode import create as qrcreate from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request from frappe.integrations.utils import make_post_request, make_get_request
@ -161,9 +162,9 @@ def get_item_list(invoice):
item.qty = abs(item.qty) item.qty = abs(item.qty)
item.discount_amount = abs(item.discount_amount * item.qty) item.discount_amount = abs(item.discount_amount * item.qty)
item.unit_rate = abs(item.base_amount / item.qty) item.unit_rate = abs(item.base_net_amount / item.qty)
item.gross_amount = abs(item.base_amount) item.gross_amount = abs(item.base_net_amount)
item.taxable_value = abs(item.base_amount) item.taxable_value = abs(item.base_net_amount)
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
@ -198,7 +199,7 @@ def update_item_taxes(invoice, item):
if t.account_head in gst_accounts_list: if t.account_head in gst_accounts_list:
item_tax_rate = item_tax_detail[0] item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount # item tax amount excluding discount amount
item_tax_amount = (item_tax_rate / 100) * item.base_amount item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
if t.account_head in gst_accounts.cess_account: if t.account_head in gst_accounts.cess_account:
item_tax_amount_after_discount = item_tax_detail[1] item_tax_amount_after_discount = item_tax_detail[1]
@ -217,8 +218,14 @@ def update_item_taxes(invoice, item):
def get_invoice_value_details(invoice): def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict()) invoice_value_details = frappe._dict(dict())
invoice_value_details.base_total = abs(invoice.base_total)
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total)
else:
invoice_value_details.base_total = abs(invoice.base_net_total)
# since tax already considers discount amount
invoice_value_details.invoice_discount_amt = 0 # invoice.base_discount_amount
invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.round_off = invoice.base_rounding_adjustment
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
@ -244,9 +251,9 @@ def update_invoice_taxes(invoice, invoice_value_details):
for tax_type in ['igst', 'cgst', 'sgst']: for tax_type in ['igst', 'cgst', 'sgst']:
if t.account_head in gst_accounts[f'{tax_type}_account']: if t.account_head in gst_accounts[f'{tax_type}_account']:
invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount) invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount)
else: else:
invoice_value_details.total_other_charges += abs(t.base_tax_amount) invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
return invoice_value_details return invoice_value_details
@ -430,7 +437,7 @@ class GSPConnector():
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi' self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
def get_credentials(self): def get_credentials(self):
@ -473,7 +480,7 @@ class GSPConnector():
"data": json.dumps(data, indent=4) if isinstance(data, dict) else data, "data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
"response": json.dumps(res, indent=4) if res else None "response": json.dumps(res, indent=4) if res else None
}) })
request_log.insert(ignore_permissions=True) request_log.save(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()
def fetch_auth_token(self): def fetch_auth_token(self):
@ -486,7 +493,8 @@ class GSPConnector():
res = self.make_request('post', self.authenticate_url, headers) res = self.make_request('post', self.authenticate_url, headers)
self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in')) self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
self.e_invoice_settings.save() self.e_invoice_settings.save(ignore_permissions=True)
self.e_invoice_settings.reload()
except Exception: except Exception:
self.log_error(res) self.log_error(res)
@ -520,7 +528,7 @@ class GSPConnector():
except Exception: except Exception:
self.log_error() self.log_error()
self.raise_error(True) self.raise_error(True)
@staticmethod @staticmethod
def get_gstin_details(gstin): def get_gstin_details(gstin):
'''fetch and cache GSTIN details''' '''fetch and cache GSTIN details'''
@ -615,7 +623,7 @@ class GSPConnector():
except Exception: except Exception:
self.log_error(data) self.log_error(data)
self.raise_error(True) self.raise_error(True)
def generate_eway_bill(self, **kwargs): def generate_eway_bill(self, **kwargs):
args = frappe._dict(kwargs) args = frappe._dict(kwargs)
@ -664,7 +672,8 @@ class GSPConnector():
'cancelRsnCode': reason, 'cancelRsnCode': reason,
'cancelRmrk': remark 'cancelRmrk': remark
}, indent=4) }, indent=4)
headers["username"] = headers["user_name"]
del headers["user_name"]
try: try:
res = self.make_request('post', self.cancel_ewaybill_url, headers, data) res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
if res.get('success'): if res.get('success'):
@ -757,26 +766,26 @@ class GSPConnector():
'label': _('IRN Generated') 'label': _('IRN Generated')
} }
self.update_invoice() self.update_invoice()
def attach_qrcode_image(self): def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype doctype = self.invoice.doctype
docname = self.invoice.name docname = self.invoice.name
filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__")
_file = frappe.new_doc('File') qr_image = io.BytesIO()
_file.update({
'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
'attached_to_doctype': doctype,
'attached_to_name': docname,
'content': 'qrcode',
'is_private': 1
})
_file.insert()
frappe.db.commit()
url = qrcreate(qrcode, error='L') url = qrcreate(qrcode, error='L')
abs_file_path = os.path.abspath(_file.get_full_path()) url.png(qr_image, scale=2, quiet_zone=1)
url.png(abs_file_path, scale=2, quiet_zone=1) _file = frappe.get_doc({
"doctype": "File",
"file_name": filename,
"attached_to_doctype": doctype,
"attached_to_name": docname,
"attached_to_field": "qrcode_image",
"is_private": 1,
"content": qr_image.getvalue()})
_file.save()
frappe.db.commit()
self.invoice.qrcode_image = _file.file_url self.invoice.qrcode_image = _file.file_url
def update_invoice(self): def update_invoice(self):

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
frappe.query_reports["IRS 1099"] = { frappe.query_reports["IRS 1099"] = {
"filters": [ "filters": [
{ {
"fieldname":"company", "fieldname": "company",
"label": __("Company"), "label": __("Company"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Company", "options": "Company",
@ -13,7 +13,7 @@ frappe.query_reports["IRS 1099"] = {
"width": 80, "width": 80,
}, },
{ {
"fieldname":"fiscal_year", "fieldname": "fiscal_year",
"label": __("Fiscal Year"), "label": __("Fiscal Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
@ -22,7 +22,7 @@ frappe.query_reports["IRS 1099"] = {
"width": 80, "width": 80,
}, },
{ {
"fieldname":"supplier_group", "fieldname": "supplier_group",
"label": __("Supplier Group"), "label": __("Supplier Group"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Supplier Group", "options": "Supplier Group",
@ -32,16 +32,16 @@ frappe.query_reports["IRS 1099"] = {
}, },
], ],
onload: function(query_report) { onload: function (query_report) {
query_report.page.add_inner_button(__("Print IRS 1099 Forms"), () => { query_report.page.add_inner_button(__("Print IRS 1099 Forms"), () => {
build_1099_print(query_report); build_1099_print(query_report);
}); });
} }
}; };
function build_1099_print(query_report){ function build_1099_print(query_report) {
let filters = JSON.stringify(query_report.get_values()); let filters = JSON.stringify(query_report.get_values());
let w = window.open('/api/method/erpnext.regional.report.irs_1099.irs_1099.irs_1099_print?' + let w = window.open('/api/method/erpnext.regional.report.irs_1099.irs_1099.irs_1099_print?' +
'&filters=' + encodeURIComponent(filters)); '&filters=' + encodeURIComponent(filters));
// w.print(); // w.print();
} }

View File

@ -1,29 +1,34 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json import json
from frappe import _, _dict
from frappe.utils import nowdate
from frappe.utils.data import fmt_money
from erpnext.accounts.utils import get_fiscal_year
from PyPDF2 import PdfFileWriter from PyPDF2 import PdfFileWriter
import frappe
from erpnext.accounts.utils import get_fiscal_year
from frappe import _
from frappe.utils import cstr, nowdate
from frappe.utils.data import fmt_money
from frappe.utils.jinja import render_template
from frappe.utils.pdf import get_pdf from frappe.utils.pdf import get_pdf
from frappe.utils.print_format import read_multi_pdf from frappe.utils.print_format import read_multi_pdf
from frappe.utils.jinja import render_template
IRS_1099_FORMS_FILE_EXTENSION = ".pdf"
def execute(filters=None): def execute(filters=None):
filters = filters if isinstance(filters, _dict) else _dict(filters) filters = filters if isinstance(filters, frappe._dict) else frappe._dict(filters)
if not filters: if not filters:
filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0])
filters.setdefault('company', frappe.db.get_default("company")) filters.setdefault('company', frappe.db.get_default("company"))
region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company }) region = frappe.db.get_value("Company",
filters={"name": filters.company},
fieldname=["country"])
if region != 'United States': if region != 'United States':
return [],[] return [], []
data = [] data = []
columns = get_columns() columns = get_columns()
@ -34,20 +39,23 @@ def execute(filters=None):
s.tax_id as "tax_id", s.tax_id as "tax_id",
SUM(gl.debit_in_account_currency) AS "payments" SUM(gl.debit_in_account_currency) AS "payments"
FROM FROM
`tabGL Entry` gl INNER JOIN `tabSupplier` s `tabGL Entry` gl
INNER JOIN `tabSupplier` s
WHERE WHERE
s.name = gl.party s.name = gl.party
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"
GROUP BY GROUP BY
gl.party gl.party
ORDER BY ORDER BY
gl.party DESC""", {"fiscal_year": filters.fiscal_year, gl.party DESC
""", {
"fiscal_year": filters.fiscal_year,
"supplier_group": filters.supplier_group, "supplier_group": filters.supplier_group,
"company": filters.company}, as_dict=True) "company": filters.company
}, as_dict=True)
return columns, data return columns, data
@ -74,7 +82,6 @@ def get_columns():
"width": 120 "width": 120
}, },
{ {
"fieldname": "payments", "fieldname": "payments",
"label": _("Total Payments"), "label": _("Total Payments"),
"fieldtype": "Currency", "fieldtype": "Currency",
@ -88,23 +95,32 @@ def irs_1099_print(filters):
if not filters: if not filters:
frappe._dict({ frappe._dict({
"company": frappe.db.get_default("Company"), "company": frappe.db.get_default("Company"),
"fiscal_year": frappe.db.get_default("fiscal_year")}) "fiscal_year": frappe.db.get_default("Fiscal Year")
})
else: else:
filters = frappe._dict(json.loads(filters)) filters = frappe._dict(json.loads(filters))
fiscal_year_doc = get_fiscal_year(fiscal_year=filters.fiscal_year, as_dict=True)
fiscal_year = cstr(fiscal_year_doc.year_start_date.year)
company_address = get_payer_address_html(filters.company) company_address = get_payer_address_html(filters.company)
company_tin = frappe.db.get_value("Company", filters.company, "tax_id") company_tin = frappe.db.get_value("Company", filters.company, "tax_id")
columns, data = execute(filters) columns, data = execute(filters)
template = frappe.get_doc("Print Format", "IRS 1099 Form").html template = frappe.get_doc("Print Format", "IRS 1099 Form").html
output = PdfFileWriter() output = PdfFileWriter()
for row in data: for row in data:
row["fiscal_year"] = fiscal_year
row["company"] = filters.company row["company"] = filters.company
row["company_tin"] = company_tin row["company_tin"] = company_tin
row["payer_street_address"] = company_address row["payer_street_address"] = company_address
row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html("Supplier", row.supplier) row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html(
"Supplier", row.supplier)
row["payments"] = fmt_money(row["payments"], precision=0, currency="USD") row["payments"] = fmt_money(row["payments"], precision=0, currency="USD")
frappe._dict(row)
pdf = get_pdf(render_template(template, row), output=output if output else None) pdf = get_pdf(render_template(template, row), output=output if output else None)
frappe.local.response.filename = filters.fiscal_year + " " + filters.company + " IRS 1099 Forms"
frappe.local.response.filename = f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}"
frappe.local.response.filecontent = read_multi_pdf(output) frappe.local.response.filecontent = read_multi_pdf(output)
frappe.local.response.type = "download" frappe.local.response.type = "download"
@ -120,36 +136,45 @@ def get_payer_address_html(company):
ORDER BY ORDER BY
address_type="Postal" DESC, address_type="Billing" DESC address_type="Postal" DESC, address_type="Billing" DESC
LIMIT 1 LIMIT 1
""", {"company": company}, as_dict=True) """, {"company": company}, as_dict=True)
address_display = ""
if address_list: if address_list:
company_address = address_list[0]["name"] company_address = address_list[0]["name"]
return frappe.get_doc("Address", company_address).get_display() address_display = frappe.get_doc("Address", company_address).get_display()
else:
return "" return address_display
def get_street_address_html(party_type, party): def get_street_address_html(party_type, party):
address_list = frappe.db.sql(""" address_list = frappe.db.sql("""
SELECT SELECT
link.parent link.parent
FROM `tabDynamic Link` link, `tabAddress` address FROM
WHERE link.parenttype = "Address" `tabDynamic Link` link,
AND link.link_name = %(party)s `tabAddress` address
ORDER BY address.address_type="Postal" DESC, WHERE
link.parenttype = "Address"
AND link.link_name = %(party)s
ORDER BY
address.address_type="Postal" DESC,
address.address_type="Billing" DESC address.address_type="Billing" DESC
LIMIT 1 LIMIT 1
""", {"party": party}, as_dict=True) """, {"party": party}, as_dict=True)
street_address = city_state = ""
if address_list: if address_list:
supplier_address = address_list[0]["parent"] supplier_address = address_list[0]["parent"]
doc = frappe.get_doc("Address", supplier_address) doc = frappe.get_doc("Address", supplier_address)
if doc.address_line2: if doc.address_line2:
street = doc.address_line1 + "<br>\n" + doc.address_line2 + "<br>\n" street_address = doc.address_line1 + "<br>\n" + doc.address_line2 + "<br>\n"
else: else:
street = doc.address_line1 + "<br>\n" street_address = doc.address_line1 + "<br>\n"
city = doc.city + ", " if doc.city else ""
city = city + doc.state + " " if doc.state else city city_state = doc.city + ", " if doc.city else ""
city = city + doc.pincode if doc.pincode else city city_state = city_state + doc.state + " " if doc.state else city_state
city += "<br>\n" city_state = city_state + doc.pincode if doc.pincode else city_state
return street, city city_state += "<br>\n"
else:
return "", "" return street_address, city_state

View File

@ -233,7 +233,8 @@ def get_stock_ledger_entries(filters):
from `tabItem` {item_conditions}) item from `tabItem` {item_conditions}) item
where item_code = item.name and where item_code = item.name and
company = %(company)s and company = %(company)s and
posting_date <= %(to_date)s posting_date <= %(to_date)s and
is_cancelled != 1
{sle_conditions} {sle_conditions}
order by posting_date, posting_time, sle.creation, actual_qty""" #nosec order by posting_date, posting_time, sle.creation, actual_qty""" #nosec
.format(item_conditions=get_item_conditions(filters), .format(item_conditions=get_item_conditions(filters),

View File

@ -214,7 +214,10 @@ class Issue(Document):
def before_insert(self): def before_insert(self):
if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) if frappe.flags.in_test:
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
else:
self.set_response_and_resolution_time()
def set_response_and_resolution_time(self, priority=None, service_level_agreement=None): def set_response_and_resolution_time(self, priority=None, service_level_agreement=None):
service_level_agreement = get_active_service_level_agreement_for(priority=priority, service_level_agreement = get_active_service_level_agreement_for(priority=priority,