"
continue
+
#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:
- html += ' {0} : {1}'.format(df.label or df.fieldname, \
- doc.get(df.fieldname))
+ if doc.get(df.fieldname):
+ formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc)
+ html += ' {0} : {1}'.format(df.label or df.fieldname, formatted_value)
+
if not has_data : has_data = True
+
if sec_on and col_on and has_data:
doc_html += section_html + html + '
'
elif sec_on and not col_on and has_data:
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index c7efbbad8d..bc1d7628ff 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -222,6 +222,11 @@ standard_queries = {
}
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": {
"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"
@@ -339,7 +344,8 @@ scheduler_events = {
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_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": [
"erpnext.setup.doctype.email_digest.email_digest.send",
@@ -408,9 +414,6 @@ regional_overrides = {
'Italy': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data',
'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate',
- },
- 'Germany': {
- 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.germany.accounts_controller.validate_regional',
}
}
user_privacy_documents = [
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 2c385e80f4..ab65260c09 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -88,7 +88,7 @@ def get_events(start, end, filters=None):
def add_assignments(events, start, end, conditions=None):
query = """select name, start_date, end_date, employee_name,
- employee, docstatus
+ employee, docstatus, shift_type
from `tabShift Assignment` where
start_date >= %(start_date)s
or end_date <= %(end_date)s
@@ -97,18 +97,40 @@ def add_assignments(events, start, end, conditions=None):
if conditions:
query += conditions
- for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True):
- e = {
- "name": d.name,
- "doctype": "Shift Assignment",
- "start_date": d.start_date,
- "end_date": d.end_date if d.end_date else nowdate(),
- "title": cstr(d.employee_name) + ": "+ \
- cstr(d.shift_type),
- "docstatus": d.docstatus
- }
- if e not in events:
- events.append(e)
+ records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True)
+ shift_timing_map = get_shift_type_timing([d.shift_type for d in records])
+
+ for d in records:
+ daily_event_start = d.start_date
+ daily_event_end = d.end_date if d.end_date else getdate()
+ delta = timedelta(days=1)
+ while daily_event_start <= daily_event_end:
+ start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time']
+ end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time']
+ daily_event_start += delta
+ e = {
+ "name": d.name,
+ "doctype": "Shift Assignment",
+ "start_date": start_timing,
+ "end_date": end_timing,
+ "title": cstr(d.employee_name) + ": "+ \
+ cstr(d.shift_type),
+ "docstatus": d.docstatus,
+ "allDay": 0
+ }
+ if e not in events:
+ events.append(e)
+
+ return events
+
+def get_shift_type_timing(shift_types):
+ shift_timing_map = {}
+ data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time'])
+
+ for d in data:
+ shift_timing_map[d.name] = d
+
+ return shift_timing_map
def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None):
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js
index 17a986deb2..bb692e1402 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js
@@ -6,14 +6,8 @@ frappe.views.calendar["Shift Assignment"] = {
"start": "start_date",
"end": "end_date",
"id": "name",
- "docstatus": 1
- },
- options: {
- header: {
- left: 'prev,next today',
- center: 'title',
- right: 'month'
- }
+ "docstatus": 1,
+ "allDay": "allDay",
},
get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events"
}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
index 6d7c3b730d..ab586bc09c 100644
--- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
@@ -26,6 +26,7 @@ def get_columns(filters):
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "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": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
{"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
@@ -42,38 +43,53 @@ def get_data(filters):
currency = erpnext.get_company_currency(filters.get('company'))
for key, qty in iteritems(pledge_values):
- row = {}
- current_value = flt(qty * loan_security_details.get(key[1])['latest_price'])
- row.update(loan_security_details.get(key[1]))
- row.update({
- 'applicant_type': applicant_type_map.get(key[0]),
- 'applicant_name': key[0],
- 'total_qty': qty,
- 'current_value': current_value,
- 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2),
- 'currency': currency
- })
+ if qty:
+ row = {}
+ 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')
- data.append(row)
+ row.update(loan_security_details.get(key[1]))
+ row.update({
+ 'applicant_type': applicant_type_map.get(key[0]),
+ 'applicant_name': key[0],
+ 'total_qty': qty,
+ 'current_value': current_value,
+ 'price_valid_upto': valid_upto,
+ 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \
+ else 0.0,
+ 'currency': currency
+ })
+
+ data.append(row)
return data
def get_loan_security_details(filters):
security_detail_map = {}
+ loan_security_price_map = {}
+ lsp_validity_map = {}
- loan_security_price_map = frappe._dict(frappe.db.sql("""
- SELECT loan_security, loan_security_price
+ loan_security_prices = frappe.db.sql("""
+ SELECT loan_security, loan_security_price, valid_upto
FROM `tabLoan Security Price` t1
WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2
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_code', 'loan_security_name', 'haircut', 'loan_security_type',
'disabled'])
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)
return security_detail_map
@@ -118,6 +134,6 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details):
applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0)
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
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
index aa0325ef35..a3e69bbfbf 100644
--- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
@@ -6,6 +6,8 @@ import frappe
import erpnext
from frappe import _
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):
@@ -31,6 +33,7 @@ def get_columns(filters):
{"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Interest %"), "fieldname": "rate_of_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},
]
@@ -50,6 +53,9 @@ def get_active_loan_details(filters):
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()
penal_interest_rate_map = get_penal_interest_rate_map()
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_interest": penal_interest_rate_map.get(loan.loan_type),
"undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")),
+ "loan_to_value": 0.0,
"currency": currency
})
loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \
+ loan['penalty']
+ if loan_wise_security_value.get(loan.loan):
+ loan['loan_to_value'] = flt((loan['principal_outstanding'] * 100) / loan_wise_security_value.get(loan.loan))
+
return loan_details
def get_sanctioned_amount_map():
@@ -121,4 +131,53 @@ def get_interest_accruals(loans):
return accrual_map
def get_penal_interest_rate_map():
- return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1))
\ No newline at end of file
+ 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, u.loan_security
+ """.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, p.loan_security
+ """.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
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
index 3ef10c0f41..adc8013c68 100644
--- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
@@ -24,6 +24,7 @@ def get_columns(filters):
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "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": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
{"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100},
@@ -39,18 +40,22 @@ def get_data(filters):
currency = erpnext.get_company_currency(filters.get('company'))
for security, value in iteritems(current_pledges):
- row = {}
- current_value = flt(value['qty'] * loan_security_details.get(security)['latest_price'])
- row.update(loan_security_details.get(security))
- row.update({
- 'total_qty': value['qty'],
- 'current_value': current_value,
- 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
- 'pledged_applicant_count': value['applicant_count'],
- 'currency': currency
- })
+ if value.get('qty'):
+ row = {}
+ 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')
- data.append(row)
+ row.update(loan_security_details.get(security))
+ row.update({
+ 'total_qty': value.get('qty'),
+ 'current_value': current_value,
+ 'price_valid_upto': valid_upto,
+ 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
+ 'pledged_applicant_count': value.get('applicant_count'),
+ 'currency': currency
+ })
+
+ data.append(row)
return data
diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json
index 992ef16d64..f190cfae75 100644
--- a/erpnext/non_profit/doctype/member/member.json
+++ b/erpnext/non_profit/doctype/member/member.json
@@ -12,7 +12,6 @@
"membership_expiry_date",
"column_break_5",
"membership_type",
- "email",
"email_id",
"image",
"customer_section",
@@ -64,13 +63,6 @@
"options": "Membership Type",
"reqd": 1
},
- {
- "fieldname": "email",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "User",
- "options": "User"
- },
{
"fieldname": "image",
"fieldtype": "Attach Image",
@@ -178,7 +170,7 @@
],
"image_field": "image",
"links": [],
- "modified": "2020-09-16 23:44:13.596948",
+ "modified": "2020-11-09 12:12:10.174647",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Member",
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 25d6b53830..04b99f93f2 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -18,8 +18,6 @@ class Member(Document):
def validate(self):
- if self.email:
- self.validate_email_type(self.email)
if self.email_id:
self.validate_email_type(self.email_id)
@@ -57,14 +55,16 @@ class Member(Document):
def make_customer_and_link(self):
if self.customer:
frappe.msgprint(_("A customer is already linked to this Member"))
- cust = create_customer(frappe._dict({
+
+ customer = create_customer(frappe._dict({
'fullname': self.member_name,
- 'email': self.email_id or self.email,
+ 'email': self.email_id,
'phone': None
}))
- self.customer = cust
+ self.customer = customer
self.save()
+ frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer))
def get_or_create_member(user_details):
diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js
index ee8a8c0a7b..573ac3319a 100644
--- a/erpnext/non_profit/doctype/membership/membership.js
+++ b/erpnext/non_profit/doctype/membership/membership.js
@@ -4,16 +4,25 @@
frappe.ui.form.on('Membership', {
setup: function(frm) {
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) {
+ if (frm.doc.__islocal)
+ return;
+
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
- frm.call("generate_invoice", {
- save: true
- }).then(() => {
- frm.reload_doc();
+ frm.call({
+ doc: frm.doc,
+ method: "generate_invoice",
+ 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) {
- frm.add_fetch('membership_type', 'amount', 'amount');
+ frm.add_fetch("membership_type", "amount", "amount");
}
});
diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json
index 7f218966a0..6da053f9fc 100644
--- a/erpnext/non_profit/doctype/membership/membership.json
+++ b/erpnext/non_profit/doctype/membership/membership.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"member",
+ "member_name",
"membership_type",
"column_break_3",
"membership_status",
@@ -46,6 +47,8 @@
{
"fieldname": "membership_status",
"fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
"label": "Membership Status",
"options": "New\nCurrent\nExpired\nPending\nCancelled"
},
@@ -122,11 +125,18 @@
"fieldtype": "Link",
"label": "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,
"links": [],
- "modified": "2020-09-19 14:28:11.532696",
+ "modified": "2021-01-21 16:31:20.032656",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",
@@ -158,7 +168,9 @@
}
],
"restrict_to_domain": "Non Profit",
+ "search_fields": "member, member_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "title_field": "member_name",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index 7d15abaa3b..c113b80d56 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -14,33 +14,43 @@ from erpnext.non_profit.doctype.member.member import create_member
from frappe import _
import erpnext
-
class Membership(Document):
def validate(self):
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:
- 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
+ self.validate_membership_period()
- if self.get("__islocal"):
- self.member = member_name
+ def create_member_from_website_user(self):
+ 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)
- last_membership = erpnext.get_last_membership()
+ last_membership = erpnext.get_last_membership(self.member)
# if person applied for offline membership
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 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)
elif frappe.session.user == "Administrator":
@@ -54,11 +64,16 @@ class Membership(Document):
self.to_date = add_months(self.from_date, 1)
def on_payment_authorized(self, status_changed_to=None):
- if status_changed_to in ("Completed", "Authorized"):
- self.load_from_db()
- self.db_set('paid', 1)
+ if status_changed_to not in ("Completed", "Authorized"):
+ return
+ 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):
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"))
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:
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
- if not settings.debit_account:
- frappe.throw(_("You need to set Debit Account in Membership Settings"))
-
- if not settings.company:
- frappe.throw(_("You need to set Default Company for invoicing in Membership Settings"))
+ plan = frappe.get_doc("Membership Type", self.membership_type)
+ settings = frappe.get_doc("Membership Settings")
+ self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, plan, settings)
self.invoice = invoice.name
+ if with_payment_entry:
+ self.make_payment_entry(settings, invoice)
+
if save:
self.save()
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 Debit Account in {0}").format(settings_link))
+
+ if not settings.company:
+ frappe.throw(_("You need to set Default Company 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 Payment Account 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):
settings = frappe.get_doc("Membership Settings")
if not settings.send_email:
- frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings"))
+ frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format(
+ get_link_to_form("Membership Settings", "Membership Settings")))
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)
- 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)]
if self.invoice and settings.send_invoice:
@@ -112,48 +157,56 @@ class Membership(Document):
}
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:
frappe.sendmail(**email_args)
def generate_and_send_invoice(self):
- invoice = self.generate_invoice(False)
+ self.generate_invoice(save=False)
self.send_acknowlement()
+
def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({
- 'doctype': 'Sales Invoice',
- 'customer': member.customer,
- 'debit_to': settings.debit_account,
- 'currency': membership.currency,
- 'is_pos': 0,
- 'items': [
+ "doctype": "Sales Invoice",
+ "customer": member.customer,
+ "debit_to": settings.debit_account,
+ "currency": membership.currency,
+ "company": settings.company,
+ "is_pos": 0,
+ "items": [
{
- 'item_code': plan.linked_item,
- 'rate': membership.amount,
- 'qty': 1
+ "item_code": plan.linked_item,
+ "rate": membership.amount,
+ "qty": 1
}
]
})
-
+ invoice.set_missing_values()
invoice.insert(ignore_permissions=True)
invoice.submit()
+ frappe.msgprint(_("Sales Invoice created successfully"))
+
return invoice
+
def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={
- 'subscription_id': subscription_id,
- 'email_id': email
+ "subscription_id": subscription_id,
+ "email_id": email
}, order_by="creation desc")
try:
- return frappe.get_doc("Member", members[0]['name'])
+ return frappe.get_doc("Member", members[0]["name"])
except:
return None
+
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")
key = settings.get_webhook_secret()
@@ -162,6 +215,7 @@ def verify_signature(data):
controller.verify_signature(data, signature, key)
+
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True)
@@ -170,16 +224,16 @@ def trigger_razorpay_subscription(*args, **kwargs):
except Exception as e:
log = frappe.log_error(e, "Webhook Verification Error")
notify_failure(log)
- return { 'status': 'Failed', 'reason': e}
+ return { "status": "Failed", "reason": e}
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
- subscription = data.payload.get("subscription", {}).get('entity', {})
+ subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
- payment = data.payload.get("payment", {}).get('entity', {})
+ payment = data.payload.get("payment", {}).get("entity", {})
payment = frappe._dict(payment)
try:
@@ -189,15 +243,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member = get_member_based_on_subscription(subscription.id, payment.email)
if not member:
member = create_member(frappe._dict({
- 'fullname': payment.email,
- 'email': payment.email,
- 'plan_id': get_plan_from_razorpay_id(subscription.plan_id)
+ "fullname": payment.email,
+ "email": payment.email,
+ "plan_id": get_plan_from_razorpay_id(subscription.plan_id)
}))
member.subscription_id = subscription.id
member.customer_id = payment.customer_id
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)
elif subscription.notes and type(subscription.notes) == str:
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)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log)
- return { 'status': 'Failed', 'reason': e}
+ return { "status": "Failed", "reason": e}
- return { 'status': 'Success' }
+ return { "status": "Success" }
def notify_failure(log):
try:
- content = """Dear System Manager,
-Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below
+ content = """
+ 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)
except:
pass
+
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:
- return plan[0]['name']
+ return plan[0]["name"]
except:
return None
+
+
+def set_expired_status():
+ frappe.db.sql("""
+ UPDATE
+ `tabMembership` SET `status` = 'Expired'
+ WHERE
+ `status` not in ('Cancelled') AND `to_date` < %s
+ """, (nowdate()))
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership/membership_list.js b/erpnext/non_profit/doctype/membership/membership_list.js
new file mode 100644
index 0000000000..a959159899
--- /dev/null
+++ b/erpnext/non_profit/doctype/membership/membership_list.js
@@ -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'];
+ }
+ }
+};
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
index b23f4062a9..ff7e6c473c 100644
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ b/erpnext/non_profit/doctype/membership/test_membership.py
@@ -2,8 +2,110 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
-
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):
- 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
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
index 1d894027b0..c95aab2a7a 100644
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
@@ -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 {
filters: {
"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 {
filters: {
"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 {
filters: {
- 'account_type': 'Receivable',
- 'is_group': 0,
- 'company': frm.doc.company
+ "account_type": "Receivable",
+ "is_group": 0,
+ "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
}
};
});
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
index 5b6bab5b0a..3887b0a2be 100644
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
@@ -11,9 +11,12 @@
"billing_frequency",
"webhook_secret",
"column_break_6",
- "enable_auto_invoicing",
+ "enable_invoicing",
+ "create_for_web_forms",
+ "make_payment_entry",
"company",
"debit_account",
+ "payment_account",
"column_break_9",
"send_email",
"send_invoice",
@@ -58,14 +61,7 @@
"label": "Invoicing"
},
{
- "default": "0",
- "fieldname": "enable_auto_invoicing",
- "fieldtype": "Check",
- "label": "Enable Auto Invoicing",
- "mandatory_depends_on": "eval:doc.send_invoice"
- },
- {
- "depends_on": "eval:doc.enable_auto_invoicing",
+ "depends_on": "eval:doc.enable_invoicing",
"fieldname": "debit_account",
"fieldtype": "Link",
"label": "Debit Account",
@@ -77,7 +73,7 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.enable_auto_invoicing",
+ "depends_on": "eval:doc.enable_invoicing",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@@ -86,7 +82,7 @@
},
{
"default": "0",
- "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email",
+ "depends_on": "eval:doc.enable_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
@@ -119,11 +115,43 @@
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"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,
"links": [],
- "modified": "2020-08-05 17:26:37.287395",
+ "modified": "2021-01-21 19:57:53.213286",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Settings",
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js
index 43311a2c96..91a5cb74ba 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.js
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.js
@@ -2,13 +2,21 @@
// For license information, please see license.txt
frappe.ui.form.on('Membership Type', {
- refresh: function(frm) {
- frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ refresh: function (frm) {
+ frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
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);
});
+
+ frm.set_query('linked_item', () => {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
}
});
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py
index b95b04316f..022829bd3a 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.py
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.py
@@ -5,9 +5,14 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
+from frappe import _
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):
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index e5bb9cd921..33c8e297c4 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -677,7 +677,7 @@ erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
erpnext.patches.v12_0.fix_quotation_expired_status
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
erpnext.patches.v12_0.rename_pos_closing_doctype
-erpnext.patches.v13_0.replace_pos_payment_mode_table
+erpnext.patches.v13_0.replace_pos_payment_mode_table #2020-12-29
erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22
erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive
execute:frappe.reload_doc("HR", "doctype", "Employee Advance")
@@ -737,13 +737,18 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll
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")
+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.create_leave_policy_assignment_based_on_employee_current_leave_policy
+erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
execute:frappe.rename_doc("Workspace", "Loan", "Loan Management", ignore_if_exists=True, force=True)
+erpnext.patches.v13_0.create_uae_pos_invoice_fields
erpnext.patches.v13_0.update_project_template_tasks
erpnext.patches.v13_0.set_company_in_leave_ledger_entry
erpnext.patches.v13_0.convert_qi_parameter_to_link_field
+erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes
+erpnext.patches.v13_0.add_naming_series_to_old_projects
diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py
new file mode 100644
index 0000000000..79b67533ed
--- /dev/null
+++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py
@@ -0,0 +1,26 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter, delete_property_setter
+
+def execute():
+ frappe.reload_doc("projects", "doctype", "project")
+ projects = frappe.db.get_all("Project",
+ fields=["name", "naming_series", "modified"],
+ filters={
+ "naming_series": ["is", "not set"]
+ },
+ order_by="timestamp(modified) asc")
+
+ # disable set only once as the old docs must be saved
+ # (to bypass 'Cant change naming series' validation on save)
+ make_property_setter("Project", "naming_series", "set_only_once", 0, "Check")
+
+ for entry in projects:
+ # need to save the doc so that users can edit old projects
+ doc = frappe.get_doc("Project", entry.name)
+ if not doc.naming_series:
+ doc.naming_series = "PROJ-.####"
+ doc.save()
+
+ delete_property_setter("Project", "set_only_once", "naming_series")
+ frappe.db.commit()
diff --git a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py
new file mode 100644
index 0000000000..48d5cb4cc8
--- /dev/null
+++ b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py
@@ -0,0 +1,14 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from erpnext.regional.united_arab_emirates.setup import make_custom_fields
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': ['in', ['Saudi Arabia', 'United Arab Emirates']]})
+ if not company:
+ return
+
+ make_custom_fields()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py
index 1ca211bf1b..7cb264830a 100644
--- a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py
+++ b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py
@@ -6,12 +6,10 @@ from __future__ import unicode_literals
import frappe
def execute():
- frappe.reload_doc("accounts", "doctype", "POS Payment Method")
+ frappe.reload_doc("accounts", "doctype", "pos_payment_method")
pos_profiles = frappe.get_all("POS Profile")
for pos_profile in pos_profiles:
- if not pos_profile.get("payments"): return
-
payments = frappe.db.sql("""
select idx, parentfield, parenttype, parent, mode_of_payment, `default` from `tabSales Invoice Payment` where parent=%s
""", pos_profile.name, as_dict=1)
diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
new file mode 100644
index 0000000000..de08aa26b3
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
@@ -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()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_member_email_address.py b/erpnext/patches/v13_0/update_member_email_address.py
new file mode 100644
index 0000000000..4056f84069
--- /dev/null
+++ b/erpnext/patches/v13_0/update_member_email_address.py
@@ -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")
diff --git a/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py
new file mode 100644
index 0000000000..42bca7c53a
--- /dev/null
+++ b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "POS Invoice Merge Log")
+ frappe.reload_doc("accounts", "doctype", "POS Closing Entry")
+ if frappe.db.count('POS Invoice Merge Log'):
+ frappe.db.sql('''
+ UPDATE
+ `tabPOS Invoice Merge Log` log, `tabPOS Invoice Reference` log_ref
+ SET
+ log.pos_closing_entry = (
+ SELECT clo_ref.parent FROM `tabPOS Invoice Reference` clo_ref
+ WHERE clo_ref.pos_invoice = log_ref.pos_invoice
+ AND clo_ref.parenttype = 'POS Closing Entry'
+ )
+ WHERE
+ log_ref.parent = log.name
+ ''')
+
+ frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Submitted' where docstatus = 1''')
+ frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Cancelled' where docstatus = 2''')
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
index 5fa062306c..8cc27d217f 100644
--- a/erpnext/patches/v13_0/update_project_template_tasks.py
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -5,40 +5,43 @@ from __future__ import unicode_literals
import frappe
def execute():
- frappe.reload_doc("projects", "doctype", "project_template")
- 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", "project_template")
+ frappe.reload_doc("projects", "doctype", "project_template_task")
+ frappe.reload_doc("projects", "doctype", "task")
- for template_name in frappe.db.sql("""
- select
- name
- 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)
+ # Update property setter status if any
+ property_setter = frappe.db.get_value('Property Setter', {'doc_type': 'Task',
+ 'field_name': 'status', 'property': 'options'})
- if replace_tasks:
- template.tasks = []
- for tsk in new_tasks:
- template.append("tasks", {
- "task": tsk.name,
- "subject": tsk.subject
- })
- template.save()
\ No newline at end of file
+ if property_setter:
+ property_setter_doc = frappe.get_doc('Property Setter', {'doc_type': 'Task',
+ 'field_name': 'status', 'property': 'options'})
+ property_setter_doc.value += "\nTemplate"
+ property_setter_doc.save()
+
+ for template_name in frappe.get_all('Project Template'):
+ 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()
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js
index 7737e6c886..d1ed91fac7 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.js
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js
@@ -12,6 +12,8 @@ frappe.ui.form.on('Additional Salary', {
}
};
});
+
+ frm.trigger('set_earning_component');
},
employee: function(frm) {
@@ -43,6 +45,19 @@ 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 {
+ filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company}
+ };
+ });
+ },
+
get_employee_currency: function(frm) {
frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
index 182ce0f83a..b2809b164a 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
@@ -10,15 +10,7 @@ frappe.ui.form.on('Employee Incentive', {
}
};
});
-
- 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}
- };
- });
-
+ frm.trigger('set_earning_component');
},
employee: function(frm) {
@@ -45,11 +37,21 @@ frappe.ui.form.on('Employee Incentive', {
callback: function(data) {
if (data.message) {
frm.set_value("company", data.message.company);
+ frm.trigger('set_earning_component');
}
}
});
},
+ set_earning_component: function(frm) {
+ if (!frm.doc.company) return;
+ frm.set_query("salary_component", function() {
+ return {
+ filters: {type: "earning", company: frm.doc.company}
+ };
+ });
+ },
+
get_employee_currency: function(frm) {
frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js
index 6c7b382264..1378bf0b91 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -58,13 +58,11 @@ frappe.ui.form.on('Salary Structure', {
if(!frm.doc.company) return;
frm.set_query("salary_component", "earnings", function() {
return {
- query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {type: "earning", company: frm.doc.company}
};
});
frm.set_query("salary_component", "deductions", function() {
return {
- query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {type: "deduction", company: frm.doc.company}
};
});
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index e71803172c..1712081550 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -207,22 +207,3 @@ def get_employees(salary_structure):
return list(set([d.employee for d in employees]))
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters):
- if len(filters) < 2:
- return {}
-
- return frappe.db.sql("""
- select t1.salary_component
- from `tabSalary Component` t1, `tabSalary Component Account` t2
- where (t1.name = t2.parent
- and t1.type = %(type)s
- and t2.company = %(company)s)
- or (t1.type = %(type)s
- and t1.statistical_component = 1)
- order by salary_component
- """,{
- "type": filters['type'],
- "company": filters['company']
- })
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index b2897852f0..4a40e8e8f9 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -48,7 +48,9 @@
"js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html",
"stock/dashboard/item_dashboard_list.html",
- "stock/dashboard/item_dashboard.js"
+ "stock/dashboard/item_dashboard.js",
+ "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html",
+ "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html"
],
"js/point-of-sale.min.js": [
"selling/page/point_of_sale/pos_item_selector.js",
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index db85a3ec99..a2a723dd77 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -195,6 +195,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
this._super(doc, cdt, cdn);
},
+ batch_no: function(doc, cdt, cdn) {
+ this._super(doc, cdt, cdn);
+ },
+
received_qty: function(doc, cdt, cdn) {
this.calculate_accepted_qty(doc, cdt, cdn)
},
@@ -516,4 +520,4 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
});
dialog.show();
-}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 12f178c82d..1db0f5f29d 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -105,10 +105,18 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.ui.form.on(this.frm.doctype + " Item", {
items_add: function(frm, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
- if(!item.warehouse && frm.doc.set_warehouse) {
+ if (!item.warehouse && frm.doc.set_warehouse) {
item.warehouse = frm.doc.set_warehouse;
}
+ if (!item.target_warehouse && frm.doc.set_target_warehouse) {
+ item.target_warehouse = frm.doc.set_target_warehouse;
+ }
+
+ if (!item.from_warehouse && frm.doc.set_from_warehouse) {
+ item.from_warehouse = frm.doc.set_from_warehouse;
+ }
+
erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items');
}
});
@@ -227,6 +235,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
};
+ this.frm.trigger('set_default_internal_warehouse');
+
return frappe.run_serially([
() => set_value('currency', currency),
() => set_value('price_list_currency', currency),
@@ -590,11 +600,21 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message &&
- (r.message.has_batch_no || r.message.has_serial_no)) {
+ (r.message.has_batch_no || r.message.has_serial_no)) {
frappe.flags.hide_serial_batch_dialog = false;
}
});
},
+ () => {
+ // check if batch serial selector is disabled or not
+ if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog)
+ return frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector')
+ .then((value) => {
+ if (value) {
+ frappe.flags.hide_serial_batch_dialog = true;
+ }
+ });
+ },
() => {
if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) {
var d = locals[cdt][cdn];
@@ -649,7 +669,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
args: item_args
},
callback: function(r) {
- frappe.model.set_value(item.doctype, item.name, 'rate', r.message);
+ frappe.model.set_value(item.doctype, item.name, 'rate', r.message * item.conversion_factor);
}
});
},
@@ -715,6 +735,31 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.calculate_taxes_and_totals(false);
},
+ update_stock: function() {
+ this.frm.trigger('set_default_internal_warehouse');
+ },
+
+ set_default_internal_warehouse: function() {
+ let me = this;
+ if ((this.frm.doc.doctype === 'Sales Invoice' && me.frm.doc.update_stock)
+ || this.frm.doc.doctype == 'Delivery Note') {
+ if (this.frm.doc.is_internal_customer && this.frm.doc.company === this.frm.doc.represents_company) {
+ frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) {
+ me.frm.set_value('set_target_warehouse', value.default_in_transit_warehouse);
+ });
+ }
+ }
+
+ if ((this.frm.doc.doctype === 'Purchase Invoice' && me.frm.doc.update_stock)
+ || this.frm.doc.doctype == 'Purchase Receipt') {
+ if (this.frm.doc.is_internal_supplier && this.frm.doc.company === this.frm.doc.represents_company) {
+ frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) {
+ me.frm.set_value('set_from_warehouse', value.default_in_transit_warehouse);
+ });
+ }
+ }
+ },
+
company: function() {
var me = this;
var set_pricing = function() {
@@ -801,7 +846,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function(){
set_party_account(set_pricing);
- })
+ });
// Get default company billing address in Purchase Invoice, Order and Receipt
frappe.call({
@@ -1100,6 +1145,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
},
+ batch_no: function(doc, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ this.apply_price_list(item, true);
+ },
+
toggle_conversion_factor: function(item) {
// toggle read only property for conversion factor field if the uom and stock uom are same
if(this.frm.get_field('items').grid.fields_map.conversion_factor) {
@@ -1404,6 +1454,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
"pricing_rules": d.pricing_rules,
"warehouse": d.warehouse,
"serial_no": d.serial_no,
+ "batch_no": d.batch_no,
"price_list_rate": d.price_list_rate,
"conversion_factor": d.conversion_factor || 1.0
});
@@ -1962,6 +2013,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse);
},
+ set_target_warehouse: function() {
+ this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse);
+ },
+
+ set_from_warehouse: function() {
+ this.autofill_warehouse(this.frm.doc.items, "from_warehouse", this.frm.doc.set_from_warehouse);
+ },
+
autofill_warehouse : function (child_table, warehouse_field, warehouse) {
if (warehouse && child_table && child_table.length) {
let doctype = child_table[0].doctype;
@@ -2026,3 +2085,35 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_
}, show_dialog);
});
}
+
+erpnext.apply_putaway_rule = (frm, purpose=null) => {
+ if (!frm.doc.company) {
+ frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")});
+ }
+ if (!frm.doc.items.length) return;
+
+ frappe.call({
+ method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule",
+ args: {
+ doctype: frm.doctype,
+ items: frm.doc.items,
+ company: frm.doc.company,
+ sync: true,
+ purpose: purpose
+ },
+ callback: (result) => {
+ if (!result.exc && result.message) {
+ frm.clear_table("items");
+
+ let items = result.message;
+ items.forEach((row) => {
+ delete row["name"]; // dont overwrite name from server side
+ let child = frm.add_child("items");
+ Object.assign(child, row);
+ frm.script_manager.trigger("qty", child.doctype, child.name);
+ });
+ frm.get_field("items").grid.refresh();
+ }
+ }
+ });
+};
\ No newline at end of file
diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js
index 770704e595..808dd5add0 100644
--- a/erpnext/public/js/utils/party.js
+++ b/erpnext/public/js/utils/party.js
@@ -276,6 +276,12 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
erpnext.utils.get_shipping_address = function(frm, callback){
if (frm.doc.company) {
+ if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference ||
+ frm.doc.internal_order_reference)) {
+ if (callback) {
+ return callback();
+ }
+ }
frappe.call({
method: "erpnext.accounts.custom.address.get_shipping_address",
args: {
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json
index 713e8e34ef..f60de4c8af 100644
--- a/erpnext/regional/doctype/datev_settings/datev_settings.json
+++ b/erpnext/regional/doctype/datev_settings/datev_settings.json
@@ -7,13 +7,14 @@
"engine": "InnoDB",
"field_order": [
"client",
- "account_number_length",
- "column_break_2",
"client_number",
- "section_break_4",
+ "column_break_2",
+ "consultant_number",
"consultant",
+ "section_break_4",
+ "account_number_length",
"column_break_6",
- "consultant_number"
+ "temporary_against_account_number"
],
"fields": [
{
@@ -66,10 +67,17 @@
"fieldtype": "Int",
"label": "Account Number Length",
"reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "temporary_against_account_number",
+ "fieldtype": "Data",
+ "label": "Temporary Against Account Number",
+ "reqd": 1
}
],
"links": [],
- "modified": "2020-11-05 17:52:11.674329",
+ "modified": "2020-11-19 19:00:09.088816",
"modified_by": "Administrator",
"module": "Regional",
"name": "DATEV Settings",
diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py
deleted file mode 100644
index 7f76493608..0000000000
--- a/erpnext/regional/germany/accounts_controller.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import frappe
-from frappe import _
-from frappe import msgprint
-
-
-REQUIRED_FIELDS = {
- "Sales Invoice": [
- {
- "field_name": "company_address",
- "regulation": "§ 14 Abs. 4 Nr. 1 UStG"
- },
- {
- "field_name": "company_tax_id",
- "regulation": "§ 14 Abs. 4 Nr. 2 UStG"
- },
- {
- "field_name": "taxes",
- "regulation": "§ 14 Abs. 4 Nr. 8 UStG"
- },
- {
- "field_name": "customer_address",
- "regulation": "§ 14 Abs. 4 Nr. 1 UStG",
- "condition": "base_grand_total > 250"
- }
- ]
-}
-
-
-def validate_regional(doc):
- """Check if required fields for this document are present."""
- required_fields = REQUIRED_FIELDS.get(doc.doctype)
- if not required_fields:
- return
-
- meta = frappe.get_meta(doc.doctype)
- field_map = {field.fieldname: field.label for field in meta.fields}
-
- for field in required_fields:
- condition = field.get("condition")
- if condition and not frappe.safe_eval(condition, doc.as_dict()):
- continue
-
- field_name = field.get("field_name")
- regulation = field.get("regulation")
- if field_name and not doc.get(field_name):
- missing(field_map.get(field_name), regulation)
-
-
-def missing(field_label, regulation):
- """Notify the user that a required field is missing."""
- translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501
- formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation)
- msgprint(formatted_msg)
diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py
deleted file mode 100644
index 8bd378c971..0000000000
--- a/erpnext/regional/germany/test_accounts_controller.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import frappe
-import unittest
-from erpnext.regional.germany.accounts_controller import validate_regional
-
-
-class TestAccountsController(unittest.TestCase):
-
- def setUp(self):
- self.sales_invoice = frappe.get_last_doc('Sales Invoice')
-
- def test_validate_regional(self):
- validate_regional(self.sales_invoice)
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 9c86cc89f5..9fa94c401f 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -18,6 +18,9 @@ erpnext.setup_einvoice_actions = (doctype) => {
if (!irn && !__unsaved) {
const action = () => {
+ if (frm.doc.__unsaved) {
+ frappe.throw(__('Please save the document to generate IRN.'));
+ }
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
args: { doctype, docname: name },
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index eb210be16a..2366fcb9ed 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -11,6 +11,7 @@ import json
import base64
import frappe
import traceback
+import io
from frappe import _, bold
from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request
@@ -436,7 +437,7 @@ class GSPConnector():
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
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'
def get_credentials(self):
@@ -527,7 +528,7 @@ class GSPConnector():
except Exception:
self.log_error()
self.raise_error(True)
-
+
@staticmethod
def get_gstin_details(gstin):
'''fetch and cache GSTIN details'''
@@ -622,7 +623,7 @@ class GSPConnector():
except Exception:
self.log_error(data)
self.raise_error(True)
-
+
def generate_eway_bill(self, **kwargs):
args = frappe._dict(kwargs)
@@ -671,7 +672,8 @@ class GSPConnector():
'cancelRsnCode': reason,
'cancelRmrk': remark
}, indent=4)
-
+ headers["username"] = headers["user_name"]
+ del headers["user_name"]
try:
res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
if res.get('success'):
@@ -769,21 +771,21 @@ class GSPConnector():
qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype
docname = self.invoice.name
+ filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__")
- _file = frappe.new_doc('File')
- _file.update({
- 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
- 'attached_to_doctype': doctype,
- 'attached_to_name': docname,
- 'content': str(base64.b64encode(os.urandom(64))),
- 'is_private': 1
- })
- _file.insert()
- frappe.db.commit()
+ qr_image = io.BytesIO()
url = qrcreate(qrcode, error='L')
- abs_file_path = os.path.abspath(_file.get_full_path())
- url.png(abs_file_path, scale=2, quiet_zone=1)
-
+ url.png(qr_image, 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
def update_invoice(self):
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index 364c0eefac..d3b7ea3b1a 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -41,14 +41,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
callback: function(r) {
if(r.message) {
frm.set_value('taxes_and_charges', r.message.taxes_and_charges);
+ frm.set_value('taxes', r.message.taxes);
frm.set_value('place_of_supply', r.message.place_of_supply);
- } else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) {
- frm.set_value('taxes_and_charges', '');
- frm.set_value('taxes', []);
}
}
});
}
});
-};
+}
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 0d8263835d..e89885f380 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -171,7 +171,7 @@ def get_regional_address_details(party_details, doctype, company):
if is_internal_transfer(party_details, doctype):
party_details.taxes_and_charges = ''
- party_details.taxes = ''
+ party_details.taxes = []
return party_details
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
diff --git a/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json b/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json
index ce8c44a9a1..e59700f5a5 100644
--- a/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json
+++ b/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json
@@ -1,23 +1,26 @@
-[
- {
- "align_labels_right": 0,
- "css": "",
- "custom_format": 1,
- "default_print_language": "en",
- "disabled": 0,
- "doc_type": "Supplier",
- "docstatus": 0,
- "doctype": "Print Format",
- "font": "Default",
- "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
PAYER'S name, street address, city or town, state or province, country, ZIP or foreign postal code, and telephone no. \n\t{{company if company else \"\"}} \n\t{{payer_street_address if payer_street_address else \"\"}}\n
\n
1 Rents
\n
OMB No. 1545-0115 2018 Form 1099-MISC
\n
Miscellaneous Income
\n
\n
\n
2 Royalties
\n
\n
\n
3 Other Income \n\t{{payments if payments else \"\"}}\n\t
\n
4 Federal Income tax withheld
\n
Copy A For Internal Revenue Service Center
File with Form 1096
\n
\n
\n
PAYER'S TIN \n\t{{company_tin if company_tin else \"\"}}\n\t
\n \n
RECIPIENT'S TIN
\n {{tax_id if tax_id else \"None\"}}\n
\n
Fishing boat proceeds
\n
6 Medical and health care payments
\n
\n
\n
RECIPIENT'S name \n {{supplier if supplier else \"\"}}\n
\n
7 Nonemployee compensation \n\t
\n
Substitute payments in lieu of dividends or interest
\n
For Privacy Act and Paperwork Reduction Act Notice, see the 2018 General Instructions for Certain Information Returns.
\n
\n
\n
Street address (including apt. no.) \n\t{{recipient_street_address if recipient_street_address else \"\"}}\n\t
\n
$___________
\n
$___________
\n
\n
\n
9 Payer made direct sales of $5,000 or more of consumer products to a buyer (recipient) for resale
\n
10 Crop insurance proceeds
\n
\n
\n
City or town, state or province, country, and ZIP or foreign postal code \n\t{{recipient_city_state if recipient_city_state else \"\"}}\n
\n
$___________
\n
\n
\n
11
\n
12
\n
\n
\n
Account number (see instructions)
\n
FACTA filing requirement
\n
2nd TIN not.
\n
13 Excess golden parachute payments $___________
\n
14 Gross proceeds paid to an attorney $___________
\n
\n
\n
15a Section 409A deferrals
\n
15b Section 409 income
\n
16 State tax withheld
\n
17 State/Payer's state no.
\n
18 State income
\n
\n
\n
$
\n
$
\n
$
\n
\n
$
\n
\n\n
\n
Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the Treasury - Internal Revenue Service
\n
\n\n \n
\n
\n
\n
\n \n
\n
PAYER'S name, street address, city or town, state or province, country, ZIP or foreign postal code, and telephone no. \n {{company if company else \"\"}} \n \t{{payer_street_address if payer_street_address else \"\"}}
\n
1 Rents
\n
OMB No. 1545-0115 2018 Form 1099-MISC
\n
Miscellaneous Income
\n
\n
\n
2 Royalties
\n
\n
\n
3 Other Income \n\t{{payments if payments else \"\"}}\n\t
\n
4 Federal Income tax withheld
\n
Copy 1 For State Tax Department
\n
\n
\n
PAYER'S TIN \n\t{{company_tin if company_tin else \"\"}}\n\t
\n
RECIPIENT'S TIN \n\t{{tax_id if tax_id else \"\"}}\n\t
\n
Fishing boat proceeds
\n
6 Medical and health care payments
\n
\n
\n
RECIPIENT'S name
\n {{supplier if supplier else \"\"}}\n
7 Nonemployee compensation \n\t
\n
Substitute payments in lieu of dividends or interest
\n
\n
\n
\n
Street address (including apt. no.) \n\t{{recipient_street_address if recipient_street_address else \"\"}}\n\t
\n
$___________
\n
$___________
\n
\n
\n
9 Payer made direct sales of $5,000 or more of consumer products to a buyer (recipient) for resale
\n
10 Crop insurance proceeds
\n
\n
\n
City or town, state or province, country, and ZIP or foreign postal code \n\t{{recipient_city_state if recipient_city_state else \"\"}}\n\t
\n
$___________
\n
\n
\n
11
\n
12
\n
\n
\n
Account number (see instructions)
\n
FACTA filing requirement
\n
2nd TIN not.
\n
13 Excess golden parachute payments $___________
\n
14 Gross proceeds paid to an attorney $___________
\n
\n
\n
15a Section 409A deferrals
\n
15b Section 409 income
\n
16 State tax withheld
\n
17 State/Payer's state no.
\n
18 State income
\n
\n
\n
$
\n
$
\n
$
\n
\n
$
\n
\n\n
\n
Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the Treasury - Internal Revenue Service
PAYER'S name, street address,\n city or town, state or province, country, ZIP or foreign postal code, and telephone no. \n {{ company or \"\" }} \n {{ payer_street_address or \"\" }}\n
Substitute payments in lieu of dividends or interest
\n
For Privacy Act and Paperwork Reduction Act Notice, see\n the 2018 General Instructions for Certain Information Returns.
\n
\n
\n
Street address (including apt. no.) \n {{ recipient_street_address or \"\" }}\n
\n
$___________
\n
$___________
\n
\n
\n
9 Payer made direct sales of $5,000 or more of consumer\n products to a buyer (recipient) for resale
\n
10 Crop insurance proceeds
\n
\n
\n
City or town, state or province, country, and ZIP or\n foreign postal code \n {{ recipient_city_state or \"\" }}\n
\n
$___________
\n
\n
\n
11
\n
12
\n
\n
\n
Account number (see instructions)
\n
FACTA filing requirement
\n
2nd TIN not.
\n
13 Excess golden parachute payments $___________
\n
14 Gross proceeds paid to an attorney $___________
\n
\n
\n
15a Section 409A deferrals
\n
15b Section 409 income
\n
16 State tax withheld
\n
17 State/Payer's state no.
\n
18 State income
\n
\n
\n
$
\n
$
\n
$
\n
\n
$
\n
\n\n
\n
Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the\n Treasury - Internal Revenue Service
\n
\n\n \n
\n
\n
\n
\n \n
\n
PAYER'S name, street address,\n city or town, state or province, country, ZIP or foreign postal code, and telephone no. \n {{ company or \"\"}}\n {{ payer_street_address or \"\" }}\n