mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-23 20:16:40 +00:00
updation Merge branch 'master' of git://github.com/webnotes/erpnext
Conflicts: erpnext/projects/doctype/ticket/ticket.py
This commit is contained in:
@@ -39,7 +39,5 @@ class DocType:
|
||||
|
||||
def validate(self):
|
||||
|
||||
r = sql("select name from `tabCustomer Group` where name = '%s' and docstatus = 2"%(self.doc.customer_group_name))
|
||||
if r:
|
||||
msgprint("%s record is trashed. To untrash please go to Setup & click on Trash."%(self.doc.customer_group_name))
|
||||
raise Exception
|
||||
if sql("select name from `tabCustomer Group` where name = %s and docstatus = 2", (self.doc.customer_group_name)):
|
||||
msgprint("%s record is trashed. To untrash please go to Setup & click on Trash."%(self.doc.customer_group_name), raise_exception = 1)
|
||||
|
||||
10
erpnext/setup/doctype/email_digest/email_digest.css
Normal file
10
erpnext/setup/doctype/email_digest/email_digest.css
Normal file
@@ -0,0 +1,10 @@
|
||||
table.profile-list {
|
||||
text-align: left;
|
||||
margin: auto;
|
||||
line-height: 250%;
|
||||
}
|
||||
|
||||
div.dialog-div {
|
||||
text-align: 'center';
|
||||
width: 100%;
|
||||
}
|
||||
104
erpnext/setup/doctype/email_digest/email_digest.js
Normal file
104
erpnext/setup/doctype/email_digest/email_digest.js
Normal file
@@ -0,0 +1,104 @@
|
||||
cur_frm.cscript.refresh = function(doc, dt, dn) {
|
||||
doc = locals[dt][dn];
|
||||
var save_msg = "You must <b>Save</b> the form before proceeding";
|
||||
var err_msg = "There was an error. One probable reason could be that you haven't saved the form. Please contact support@erpnext.com if the problem persists."
|
||||
|
||||
cur_frm.add_custom_button('View Now', function() {
|
||||
if(doc.__unsaved != 1) {
|
||||
$c_obj(make_doclist(dt, dn), 'get', '', function(r, rt) {
|
||||
if(r.exc) {
|
||||
msgprint(err_msg);
|
||||
console.log(r.exc);
|
||||
} else {
|
||||
//console.log(arguments);
|
||||
var d = new wn.widgets.Dialog({
|
||||
title: 'Email Digest: ' + dn,
|
||||
width: 800
|
||||
});
|
||||
|
||||
$a(d.body, 'div', '', '', r['message'][1]);
|
||||
|
||||
d.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
msgprint(save_msg);
|
||||
}
|
||||
}, 1);
|
||||
cur_frm.add_custom_button('Send Now', function() {
|
||||
if(doc.__unsaved != 1) {
|
||||
$c_obj(make_doclist(dt, dn), 'send', '', function(r, rt) {
|
||||
if(r.exc) {
|
||||
msgprint(err_msg);
|
||||
console.log(r.exc);
|
||||
} else {
|
||||
//console.log(arguments);
|
||||
msgprint('Message Sent');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
msgprint(save_msg);
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
|
||||
cur_frm.cscript['Add/Remove Recipients'] = function(doc, dt, dn) {
|
||||
// Get profile list
|
||||
$c_obj(make_doclist(dt, dn), 'get_profiles', '', function(r, rt) {
|
||||
if(r.exc) {
|
||||
msgprint(r.exc);
|
||||
} else {
|
||||
// Open a dialog and display checkboxes against email addresses
|
||||
doc = locals[dt][dn];
|
||||
var d = new wn.widgets.Dialog({
|
||||
title: 'Add/Remove Recipients',
|
||||
width: 400
|
||||
});
|
||||
var dialog_div = $a(d.body, 'div', 'dialog-div', '', '');
|
||||
var tab = make_table(dialog_div, r.profile_list.length+2, 2, '', ['15%', '85%']);
|
||||
tab.className = 'profile-list';
|
||||
var add_or_update = 'Add';
|
||||
$.each(r.profile_list, function(i, v) {
|
||||
var check = $a_input($td(tab, i+1, 0), 'checkbox');
|
||||
check.value = v.name;
|
||||
if(v.checked==1) {
|
||||
check.checked = 1;
|
||||
add_or_update = 'Update';
|
||||
}
|
||||
if(v.enabled==0) {
|
||||
v.name = "<span style='color: red'>" + v.name + " (disabled user)</span>"
|
||||
}
|
||||
var profile = $a($td(tab, i+1, 1), 'span', '', '', v.name);
|
||||
//profile.onclick = function() { check.checked = !check.checked; }
|
||||
});
|
||||
|
||||
// Display add recipients button
|
||||
if(r.profile_list.length>15) {
|
||||
$btn($td(tab, 0, 1), add_or_update + ' Recipients', function() {
|
||||
cur_frm.cscript.add_to_rec_list(doc, tab, r.profile_list.length);
|
||||
});
|
||||
}
|
||||
$btn($td(tab, r.profile_list.length+1, 1), add_or_update + ' Recipients', function() {
|
||||
cur_frm.cscript.add_to_rec_list(doc, tab, r.profile_list.length);
|
||||
});
|
||||
|
||||
cur_frm.rec_dialog = d;
|
||||
d.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cur_frm.cscript.add_to_rec_list = function(doc, tab, length) {
|
||||
// add checked profiles to list of recipients
|
||||
var rec_list = [];
|
||||
for(var i = 1; i <= length; i++) {
|
||||
var input = $($td(tab, i, 0)).find('input');
|
||||
if(input.is(':checked')) {
|
||||
rec_list.push(input.attr('value'));
|
||||
}
|
||||
}
|
||||
doc.recipient_list = rec_list.join('\n');
|
||||
//console.log(doc.recipient_list);
|
||||
cur_frm.rec_dialog.hide();
|
||||
cur_frm.refresh_fields();
|
||||
}
|
||||
739
erpnext/setup/doctype/email_digest/email_digest.py
Normal file
739
erpnext/setup/doctype/email_digest/email_digest.py
Normal file
@@ -0,0 +1,739 @@
|
||||
import webnotes
|
||||
|
||||
class DocType:
|
||||
def __init__(self, doc, doclist=[]):
|
||||
self.doc, self.doclist = doc, doclist
|
||||
self.sending = False
|
||||
|
||||
|
||||
def get_profiles(self):
|
||||
"""
|
||||
Get a list of profiles
|
||||
"""
|
||||
import webnotes
|
||||
profile_list = webnotes.conn.sql("""
|
||||
SELECT name, enabled FROM tabProfile
|
||||
WHERE docstatus=0 AND name NOT IN ('Administrator', 'Guest')
|
||||
ORDER BY enabled DESC, name ASC""", as_dict=1)
|
||||
if self.doc.recipient_list:
|
||||
recipient_list = self.doc.recipient_list.split("\n")
|
||||
else:
|
||||
recipient_list = []
|
||||
for p in profile_list:
|
||||
if p['name'] in recipient_list: p['checked'] = 1
|
||||
else: p['checked'] = 0
|
||||
webnotes.response['profile_list'] = profile_list
|
||||
|
||||
|
||||
def get_standard_data(self):
|
||||
"""
|
||||
Executes standard queries
|
||||
"""
|
||||
res = {}
|
||||
query_dict = {
|
||||
|
||||
'invoiced_amount': self.generate_gle_query({
|
||||
'type': 'invoiced_amount',
|
||||
'field': 'debit',
|
||||
'master_type': 'Customer',
|
||||
}),
|
||||
|
||||
'payables': self.generate_gle_query({
|
||||
'type': 'payables',
|
||||
'field': 'credit',
|
||||
'master_type': 'Supplier',
|
||||
}),
|
||||
|
||||
'collections': self.generate_gle_query({
|
||||
'type': 'collections',
|
||||
'field': 'credit',
|
||||
'master_type': 'Customer',
|
||||
}),
|
||||
|
||||
'payments': self.generate_gle_query({
|
||||
'type': 'payments',
|
||||
'field': 'debit',
|
||||
'master_type': 'Supplier',
|
||||
}),
|
||||
|
||||
'income': self.generate_gle_query({
|
||||
'type': 'income',
|
||||
'debit_or_credit': 'Credit'
|
||||
}),
|
||||
|
||||
'expenses_booked': self.generate_gle_query({
|
||||
'type': 'expenses_booked',
|
||||
'debit_or_credit': 'Debit'
|
||||
}),
|
||||
|
||||
'bank_balance': self.generate_gle_query({
|
||||
'type': 'bank_balance'
|
||||
}),
|
||||
|
||||
'new_leads': self.generate_new_type_query({
|
||||
'type': 'new_leads',
|
||||
'doctype': 'Lead'
|
||||
}),
|
||||
|
||||
'new_enquiries': self.generate_new_type_query({
|
||||
'type': 'new_enquiries',
|
||||
'doctype': 'Enquiry'
|
||||
}),
|
||||
|
||||
'new_quotations': self.generate_new_type_query({
|
||||
'type': 'new_quotations',
|
||||
'doctype': 'Quotation',
|
||||
'sum_col': 'grand_total'
|
||||
}),
|
||||
|
||||
'new_sales_orders': self.generate_new_type_query({
|
||||
'type': 'new_sales_orders',
|
||||
'doctype': 'Receivable Voucher',
|
||||
'sum_col': 'grand_total'
|
||||
}),
|
||||
|
||||
'new_purchase_orders': self.generate_new_type_query({
|
||||
'type': 'new_purchase_orders',
|
||||
'doctype': 'Purchase Order',
|
||||
'sum_col': 'grand_total'
|
||||
}),
|
||||
|
||||
'new_transactions': self.generate_new_type_query({
|
||||
'type': 'new_transactions',
|
||||
'doctype': 'Feed'
|
||||
}),
|
||||
|
||||
'stock_below_rl': ""
|
||||
}
|
||||
|
||||
result = {}
|
||||
|
||||
for query in query_dict.keys():
|
||||
if self.doc.fields[query] and query_dict[query]:
|
||||
#webnotes.msgprint(query)
|
||||
res = webnotes.conn.sql(query_dict[query], as_dict=1)
|
||||
if query == 'income':
|
||||
for r in res:
|
||||
r['value'] = float(r['credit'] - r['debit'])
|
||||
elif query in ['expenses_booked', 'bank_balance']:
|
||||
for r in res:
|
||||
r['value'] = float(r['debit'] - r['credit'])
|
||||
#webnotes.msgprint(query)
|
||||
#webnotes.msgprint(res)
|
||||
result[query] = (res and len(res)==1) and res[0] or (res and res or None)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_gle_query(self, args):
|
||||
"""
|
||||
Returns generated query string based 'tabGL Entry' and 'tabAccount'
|
||||
"""
|
||||
self.process_args(args)
|
||||
|
||||
query = None
|
||||
|
||||
if args['type'] in ['invoiced_amount', 'payables']:
|
||||
query = """
|
||||
SELECT
|
||||
IFNULL(SUM(IFNULL(gle.%(field)s, 0)), 0) AS '%(field)s',
|
||||
%(common_select)s
|
||||
FROM
|
||||
%(common_from)s
|
||||
WHERE
|
||||
%(common_where)s AND
|
||||
ac.master_type = '%(master_type)s' AND
|
||||
%(start_date_condition)s AND
|
||||
%(end_date_condition)s""" % args
|
||||
|
||||
elif args['type'] in ['collections', 'payments']:
|
||||
args['bc_accounts_regex'] = self.get_bc_accounts_regex()
|
||||
if args['bc_accounts_regex']:
|
||||
query = """
|
||||
SELECT
|
||||
IFNULL(SUM(IFNULL(gle.%(field)s, 0)), 0) AS '%(field)s',
|
||||
%(common_select)s
|
||||
FROM
|
||||
%(common_from)s
|
||||
WHERE
|
||||
%(common_where)s AND
|
||||
ac.master_type = '%(master_type)s' AND
|
||||
gle.against REGEXP '%(bc_accounts_regex)s' AND
|
||||
%(start_date_condition)s AND
|
||||
%(end_date_condition)s""" % args
|
||||
|
||||
elif args['type'] in ['income', 'expenses_booked']:
|
||||
query = """
|
||||
SELECT
|
||||
IFNULL(SUM(IFNULL(gle.debit, 0)), 0) AS 'debit',
|
||||
IFNULL(SUM(IFNULL(gle.credit, 0)), 0) AS 'credit',
|
||||
%(common_select)s
|
||||
FROM
|
||||
%(common_from)s
|
||||
WHERE
|
||||
%(common_where)s AND
|
||||
ac.is_pl_account = 'Yes' AND
|
||||
ac.debit_or_credit = '%(debit_or_credit)s' AND
|
||||
%(start_date_condition)s AND
|
||||
%(end_date_condition)s""" % args
|
||||
|
||||
elif args['type'] == 'bank_balance':
|
||||
query = """
|
||||
SELECT
|
||||
ac.account_name AS 'name',
|
||||
IFNULL(SUM(IFNULL(gle.debit, 0)), 0) AS 'debit',
|
||||
IFNULL(SUM(IFNULL(gle.credit, 0)), 0) AS 'credit',
|
||||
%(common_select)s
|
||||
FROM
|
||||
%(common_from)s
|
||||
WHERE
|
||||
%(common_where)s AND
|
||||
ac.account_type = 'Bank or Cash' AND
|
||||
%(end_date_condition)s
|
||||
GROUP BY
|
||||
ac.account_name""" % args
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def process_args(self, args):
|
||||
"""
|
||||
Adds common conditions in dictionary "args"
|
||||
"""
|
||||
start_date, end_date = self.get_start_end_dates()
|
||||
|
||||
if 'new' in args['type']:
|
||||
args.update({
|
||||
'company': self.doc.company,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'sum_if_reqd': ''
|
||||
})
|
||||
if args['type'] in ['new_quotations', 'new_sales_orders', 'new_purchase_orders']:
|
||||
args['sum_if_reqd'] = "IFNULL(SUM(IFNULL(%(sum_col)s, 0)), 0) AS '%(sum_col)s'," % args
|
||||
|
||||
if args['type'] == 'new_transactions':
|
||||
args['company_condition'] = ''
|
||||
else:
|
||||
args['company_condition'] = "company = '%(company)s' AND" % args
|
||||
|
||||
else:
|
||||
args.update({
|
||||
'common_select': "COUNT(*) AS 'count'",
|
||||
|
||||
'common_from': "`tabGL Entry` gle, `tabAccount` ac",
|
||||
|
||||
'common_where': """
|
||||
gle.company = '%s' AND
|
||||
gle.account = ac.name AND
|
||||
ac.docstatus < 2 AND
|
||||
IFNULL(gle.is_cancelled, 'No') = 'No'""" % self.doc.company,
|
||||
|
||||
'start_date_condition': "gle.posting_date >= '%s'" % start_date,
|
||||
|
||||
'end_date_condition': "gle.posting_date <= '%s'" % end_date
|
||||
})
|
||||
|
||||
|
||||
def get_start_end_dates(self):
|
||||
"""
|
||||
Returns start and end date depending on the frequency of email digest
|
||||
"""
|
||||
from datetime import datetime, date, timedelta
|
||||
today = datetime.now().date()
|
||||
year, month, day = today.year, today.month, today.day
|
||||
|
||||
if self.doc.frequency == 'Daily':
|
||||
if self.sending:
|
||||
start_date = end_date = today - timedelta(days=1)
|
||||
else:
|
||||
start_date = end_date = today
|
||||
|
||||
elif self.doc.frequency == 'Weekly':
|
||||
if self.sending:
|
||||
start_date = today - timedelta(days=today.weekday(), weeks=1)
|
||||
end_date = start_date + timedelta(days=6)
|
||||
else:
|
||||
start_date = today - timedelta(days=today.weekday())
|
||||
end_date = start_date + timedelta(days=6)
|
||||
|
||||
else:
|
||||
import calendar
|
||||
|
||||
if self.sending:
|
||||
if month == 1:
|
||||
year = year - 1
|
||||
prev_month = 12
|
||||
else:
|
||||
prev_month = month - 1
|
||||
start_date = date(year, prev_month, 1)
|
||||
last_day = calendar.monthrange(year, prev_month)[1]
|
||||
end_date = date(year, prev_month, last_day)
|
||||
else:
|
||||
start_date = date(year, month, 1)
|
||||
last_day = calendar.monthrange(year, month)[1]
|
||||
end_date = date(year, month, last_day)
|
||||
|
||||
return start_date, end_date
|
||||
|
||||
|
||||
def generate_new_type_query(self, args):
|
||||
"""
|
||||
Returns generated query string for calculating new transactions created
|
||||
"""
|
||||
self.process_args(args)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
%(sum_if_reqd)s
|
||||
COUNT(*) AS 'count'
|
||||
FROM
|
||||
`tab%(doctype)s`
|
||||
WHERE
|
||||
docstatus < 2 AND
|
||||
%(company_condition)s
|
||||
creation >= '%(start_date)s' AND
|
||||
creation <= '%(end_date)s'""" % args
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_bc_accounts_regex(self):
|
||||
"""
|
||||
Returns a regular expression of 'Bank or Cash' type account list
|
||||
"""
|
||||
bc_account_list = webnotes.conn.sql("""
|
||||
SELECT name
|
||||
FROM `tabAccount`
|
||||
WHERE account_type = 'Bank or Cash'""", as_list=1)
|
||||
|
||||
if bc_account_list:
|
||||
return '(' + '|'.join([ac[0] for ac in bc_account_list]) + ')'
|
||||
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
* Execute Query
|
||||
* Prepare Email Body from Print Format
|
||||
"""
|
||||
result, email_body = self.execute_queries()
|
||||
#webnotes.msgprint(result)
|
||||
#webnotes.msgprint(email_body)
|
||||
return result, email_body
|
||||
|
||||
|
||||
def execute_queries(self):
|
||||
"""
|
||||
* If standard==1, execute get_standard_data
|
||||
* If standard==0, execute python code in custom_code field
|
||||
"""
|
||||
result = {}
|
||||
if int(self.doc.use_standard)==1:
|
||||
result = self.get_standard_data()
|
||||
email_body = self.get_standard_body(result)
|
||||
else:
|
||||
result, email_body = self.execute_custom_code(self.doc)
|
||||
|
||||
#webnotes.msgprint(result)
|
||||
|
||||
return result, email_body
|
||||
|
||||
|
||||
def execute_custom_code(self, doc):
|
||||
"""
|
||||
Execute custom python code
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def send(self):
|
||||
"""
|
||||
* Execute get method
|
||||
* Send email to recipients
|
||||
"""
|
||||
self.sending = True
|
||||
result, email_body = self.get()
|
||||
recipient_list = self.doc.recipient_list.split("\n")
|
||||
|
||||
# before sending, check if user is disabled or not
|
||||
# do not send if disabled
|
||||
profile_list = webnotes.conn.sql("SELECT name, enabled FROM tabProfile", as_dict=1)
|
||||
for profile in profile_list:
|
||||
if profile['name'] in recipient_list and profile['enabled'] == 0:
|
||||
del recipient_list[recipient_list.index(profile['name'])]
|
||||
|
||||
from webnotes.utils.email_lib import sendmail
|
||||
try:
|
||||
sendmail(
|
||||
recipients=recipient_list,
|
||||
sender='notifications+email_digest@erpnext.com',
|
||||
reply_to='support@erpnext.com',
|
||||
subject=self.doc.frequency + ' Digest',
|
||||
msg=email_body,
|
||||
from_defs=1
|
||||
)
|
||||
except Exception, e:
|
||||
webnotes.msgprint('There was a problem in sending your email. Please contact support@erpnext.com')
|
||||
#webnotes.errprint(webnotes.getTraceback())
|
||||
|
||||
|
||||
def on_update(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
import webnotes
|
||||
args = {
|
||||
'db_name': webnotes.conn.get_value('Control Panel', '', 'account_id'),
|
||||
'event': 'setup.doctype.email_digest.email_digest.send'
|
||||
}
|
||||
from webnotes.utils.scheduler import Scheduler
|
||||
#print "before scheduler"
|
||||
sch = Scheduler()
|
||||
sch.connect()
|
||||
|
||||
|
||||
if self.doc.enabled == 1:
|
||||
# Create scheduler entry
|
||||
res = sch.conn.sql("""
|
||||
SELECT * FROM Event
|
||||
WHERE
|
||||
db_name = %(db_name)s AND
|
||||
event = %(event)s
|
||||
""", args)
|
||||
|
||||
if not (res and res[0]):
|
||||
args['next_execution'] = self.get_next_execution()
|
||||
|
||||
sch.conn.sql("""
|
||||
INSERT INTO Event (db_name, event, `interval`, next_execution, recurring)
|
||||
VALUES (%(db_name)s, %(event)s, 86400, %(next_execution)s, 1)
|
||||
""", args)
|
||||
|
||||
else:
|
||||
# delete scheduler entry
|
||||
sch.clear(args['db_name'], args['event'])
|
||||
#print "after on update"
|
||||
|
||||
|
||||
def get_next_sending(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
# Get TimeZone
|
||||
# Get System TimeZone
|
||||
import time
|
||||
from pytz import timezone
|
||||
import datetime
|
||||
import webnotes.defs
|
||||
cp = webnotes.model.doc.Document('Control Panel','Control Panel')
|
||||
app_tz = timezone(cp.time_zone)
|
||||
server_tz = timezone(getattr(webnotes.defs, 'system_timezone'))
|
||||
|
||||
start_date, end_date = self.get_start_end_dates()
|
||||
|
||||
new_date = end_date + datetime.timedelta(days=1)
|
||||
new_time = datetime.time(hour=6)
|
||||
|
||||
naive_dt = datetime.datetime.combine(new_date, new_time)
|
||||
app_dt = app_tz.localize(naive_dt)
|
||||
server_dt = server_tz.normalize(app_dt.astimezone(server_tz))
|
||||
|
||||
res = {
|
||||
'app_dt': app_dt.replace(tzinfo=None),
|
||||
'app_tz': app_tz,
|
||||
'server_dt': server_dt.replace(tzinfo=None),
|
||||
'server_tz': server_tz
|
||||
}
|
||||
|
||||
from webnotes.utils import formatdate
|
||||
str_date = formatdate(str(res['app_dt'].date()))
|
||||
str_time = res['app_dt'].time().strftime('%I:%M')
|
||||
|
||||
self.doc.next_send = str_date + " at about " + str_time
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def get_next_execution(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
dt_args = self.get_next_sending()
|
||||
server_dt = dt_args['server_dt']
|
||||
now_dt = datetime.now(dt_args['server_tz'])
|
||||
if now_dt.time() <= server_dt.time():
|
||||
next_date = now_dt.date()
|
||||
else:
|
||||
next_date = now_dt.date() + timedelta(days=1)
|
||||
|
||||
next_time = server_dt.time()
|
||||
|
||||
return datetime.combine(next_date, next_time)
|
||||
|
||||
|
||||
def onload(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
self.get_next_sending()
|
||||
|
||||
|
||||
def get_standard_body(self, result):
|
||||
"""
|
||||
Generate email body depending on the result
|
||||
"""
|
||||
from webnotes.utils import fmt_money
|
||||
from webnotes.model.doc import Document
|
||||
company = Document('Company', self.doc.company)
|
||||
currency = company.default_currency
|
||||
|
||||
def table(args):
|
||||
if type(args['body']) == type(''):
|
||||
table_body = """\
|
||||
<tbody><tr>
|
||||
<td style='padding: 5px; font-size: 24px; \
|
||||
font-weight: bold; background: #F7F7F5'>""" + \
|
||||
args['body'] + \
|
||||
"""\
|
||||
</td>
|
||||
</tr></tbody>"""
|
||||
|
||||
elif type(args['body'] == type([])):
|
||||
body_rows = []
|
||||
for rows in args['body']:
|
||||
for r in rows:
|
||||
body_rows.append("""\
|
||||
<tr>
|
||||
<td style='padding: 5px; font-size: 24px; \
|
||||
font-weight: bold; background: #F7F7F5'>""" \
|
||||
+ r + """\
|
||||
</td>
|
||||
</tr>""")
|
||||
|
||||
body_rows.append("<tr><td style='background: #F7F7F5'><br></td></tr>")
|
||||
|
||||
table_body = "<tbody>" + "".join(body_rows) + "</tbody>"
|
||||
|
||||
table_head = """\
|
||||
<thead><tr>
|
||||
<td style='padding: 5px; background: #D8D8D4; font-size: 16px; font-weight: bold'>""" \
|
||||
+ args['head'] + """\
|
||||
</td>
|
||||
</tr></thead>"""
|
||||
|
||||
return "<table style='border-collapse: collapse; width: 100%;'>" \
|
||||
+ table_head \
|
||||
+ table_body \
|
||||
+ "</table>"
|
||||
|
||||
currency_amount_str = "<span style='color: grey; font-size: 12px'>%s</span> %s"
|
||||
|
||||
body_dict = {
|
||||
|
||||
'invoiced_amount': {
|
||||
'table': 'invoiced_amount' in result and table({
|
||||
'head': 'Invoiced Amount',
|
||||
'body': currency_amount_str \
|
||||
% (currency, fmt_money(result['invoiced_amount']['debit']))
|
||||
}),
|
||||
'idx': 300
|
||||
},
|
||||
|
||||
'payables': {
|
||||
'table': 'payables' in result and table({
|
||||
'head': 'Payables',
|
||||
'body': currency_amount_str \
|
||||
% (currency, fmt_money(result['payables']['credit']))
|
||||
}),
|
||||
'idx': 200
|
||||
},
|
||||
|
||||
'collections': {
|
||||
'table': 'collections' in result and table({
|
||||
'head': 'Collections',
|
||||
'body': currency_amount_str \
|
||||
% (currency, fmt_money(result['collections']['credit']))
|
||||
}),
|
||||
'idx': 301
|
||||
},
|
||||
|
||||
'payments': {
|
||||
'table': 'payments' in result and table({
|
||||
'head': 'Payments',
|
||||
'body': currency_amount_str \
|
||||
% (currency, fmt_money(result['payments']['debit']))
|
||||
}),
|
||||
'idx': 201
|
||||
},
|
||||
|
||||
'income': {
|
||||
'table': 'income' in result and table({
|
||||
'head': 'Income',
|
||||
'body': currency_amount_str \
|
||||
% (currency, fmt_money(result['income']['value']))
|
||||
}),
|
||||
'idx': 302
|
||||
},
|
||||
|
||||
'expenses_booked': {
|
||||
'table': 'expenses_booked' in result and table({
|
||||
'head': 'Expenses Booked',
|
||||
'body': currency_amount_str \
|
||||
% (currency, fmt_money(result['expenses_booked']['value']))
|
||||
}),
|
||||
'idx': 202
|
||||
},
|
||||
|
||||
'bank_balance': {
|
||||
'table': 'bank_balance' in result and table({
|
||||
'head': 'Bank Balance',
|
||||
'body': [
|
||||
[
|
||||
"<span style='font-size: 16px; font-weight: normal'>%s</span>" % bank['name'],
|
||||
currency_amount_str % (currency, fmt_money(bank['value']))
|
||||
] for bank in result['bank_balance']
|
||||
]
|
||||
}),
|
||||
'idx': 400
|
||||
},
|
||||
|
||||
'new_leads': {
|
||||
'table': 'new_leads' in result and table({
|
||||
'head': 'New Leads',
|
||||
'body': '%s' % result['new_leads']['count']
|
||||
}),
|
||||
'idx': 100
|
||||
},
|
||||
|
||||
'new_enquiries': {
|
||||
'table': 'new_enquiries' in result and table({
|
||||
'head': 'New Enquiries',
|
||||
'body': '%s' % result['new_enquiries']['count']
|
||||
}),
|
||||
'idx': 101
|
||||
},
|
||||
|
||||
'new_quotations': {
|
||||
'table': 'new_quotations' in result and table({
|
||||
'head': 'New Quotations',
|
||||
'body': '%s' % result['new_quotations']['count']
|
||||
}),
|
||||
'idx': 102
|
||||
},
|
||||
|
||||
'new_sales_orders': {
|
||||
'table': 'new_sales_orders' in result and table({
|
||||
'head': 'New Sales Orders',
|
||||
'body': '%s' % result['new_sales_orders']['count']
|
||||
}),
|
||||
'idx': 103
|
||||
},
|
||||
|
||||
'new_purchase_orders': {
|
||||
'table': 'new_purchase_orders' in result and table({
|
||||
'head': 'New Purchase Orders',
|
||||
'body': '%s' % result['new_purchase_orders']['count']
|
||||
}),
|
||||
'idx': 104
|
||||
},
|
||||
|
||||
'new_transactions': {
|
||||
'table': 'new_transactions' in result and table({
|
||||
'head': 'New Transactions',
|
||||
'body': '%s' % result['new_transactions']['count']
|
||||
}),
|
||||
'idx': 105
|
||||
}
|
||||
|
||||
#'stock_below_rl':
|
||||
}
|
||||
|
||||
table_list = []
|
||||
|
||||
# Sort these keys depending on idx value
|
||||
bd_keys = sorted(body_dict, key=lambda x: body_dict[x]['idx'])
|
||||
|
||||
for k in bd_keys:
|
||||
if self.doc.fields[k]:
|
||||
if k in result:
|
||||
table_list.append(body_dict[k]['table'])
|
||||
elif k in ['collections', 'payments']:
|
||||
table_list.append(\
|
||||
"<div style='font-size: 16px; color: grey'>[" + \
|
||||
k.capitalize() + \
|
||||
"]<br />Missing: Ledger of type 'Bank or Cash'\
|
||||
</div>")
|
||||
|
||||
i = 0
|
||||
result = []
|
||||
op_len = len(table_list)
|
||||
while(True):
|
||||
if i>=op_len:
|
||||
break
|
||||
elif (op_len - i) == 1:
|
||||
result.append("""\
|
||||
<tr>
|
||||
<td style='width: 50%%; vertical-align: top;'>%s</td>
|
||||
<td></td>
|
||||
</tr>""" % (table_list[i]))
|
||||
else:
|
||||
result.append("""\
|
||||
<tr>
|
||||
<td style='width: 50%%; vertical-align: top;'>%s</td>
|
||||
<td>%s</td>
|
||||
</tr>""" % (table_list[i], table_list[i+1]))
|
||||
|
||||
i = i + 2
|
||||
|
||||
from webnotes.utils import formatdate
|
||||
start_date, end_date = self.get_start_end_dates()
|
||||
digest_daterange = self.doc.frequency=='Daily' \
|
||||
and formatdate(str(start_date)) \
|
||||
or (formatdate(str(start_date)) + " to " + (formatdate(str(end_date))))
|
||||
|
||||
email_body = """
|
||||
<div style='width: 100%%'>
|
||||
<div style='padding: 10px; margin: auto; text-align: center; line-height: 80%%'>
|
||||
<p style='font-weight: bold; font-size: 24px'>%s</p>
|
||||
<p style='font-size: 16px; color: grey'>%s</p>
|
||||
<p style='font-size: 20px; font-weight: bold'>%s</p>
|
||||
</div>
|
||||
<table cellspacing=15 style='width: 100%%'>""" \
|
||||
% ((self.doc.frequency + " Digest"), \
|
||||
digest_daterange, self.doc.company) \
|
||||
+ "".join(result) + """\
|
||||
</table><br><p></p>
|
||||
</div>"""
|
||||
|
||||
return email_body
|
||||
|
||||
|
||||
def send():
|
||||
"""
|
||||
|
||||
"""
|
||||
edigest_list = webnotes.conn.sql("""
|
||||
SELECT name FROM `tabEmail Digest`
|
||||
WHERE enabled=1
|
||||
""", as_list=1)
|
||||
|
||||
from webnotes.model.code import get_obj
|
||||
from datetime import datetime, timedelta
|
||||
now = datetime.now()
|
||||
now_date = now.date()
|
||||
now_time = (now + timedelta(hours=2)).time()
|
||||
|
||||
for ed in edigest_list:
|
||||
if ed[0]:
|
||||
ed_obj = get_obj('Email Digest', ed[0])
|
||||
ed_obj.sending = True
|
||||
dt_dict = ed_obj.get_next_sending()
|
||||
send_date = dt_dict['server_dt'].date()
|
||||
send_time = dt_dict['server_dt'].time()
|
||||
|
||||
if (now_date == send_date) and (send_time <= now_time):
|
||||
#webnotes.msgprint('sending ' + ed_obj.doc.name)
|
||||
ed_obj.send()
|
||||
#else:
|
||||
# webnotes.msgprint('not sending ' + ed_obj.doc.name)
|
||||
@@ -3,24 +3,25 @@
|
||||
|
||||
# These values are common in all dictionaries
|
||||
{
|
||||
'creation': '2011-07-27 14:23:09',
|
||||
'creation': '2011-12-12 10:41:40',
|
||||
'docstatus': 0,
|
||||
'modified': '2011-07-27 17:32:27',
|
||||
'modified': '2011-12-15 13:53:08',
|
||||
'modified_by': 'Administrator',
|
||||
'owner': 'Administrator'
|
||||
},
|
||||
|
||||
# These values are common for all DocType
|
||||
{
|
||||
'_last_update': '1311760331',
|
||||
'_last_update': '1323353260',
|
||||
'autoname': 'Prompt',
|
||||
'colour': 'White:FFF',
|
||||
'doctype': 'DocType',
|
||||
'issingle': 1,
|
||||
'document_type': 'System',
|
||||
'module': 'Setup',
|
||||
'name': '__common__',
|
||||
'section_style': 'Simple',
|
||||
'show_in_menu': 0,
|
||||
'version': 4
|
||||
'version': 79
|
||||
},
|
||||
|
||||
# These values are common for all DocField
|
||||
@@ -29,23 +30,18 @@
|
||||
'name': '__common__',
|
||||
'parent': 'Email Digest',
|
||||
'parentfield': 'fields',
|
||||
'parenttype': 'DocType',
|
||||
'permlevel': 0
|
||||
'parenttype': 'DocType'
|
||||
},
|
||||
|
||||
# These values are common for all DocPerm
|
||||
{
|
||||
'create': 1,
|
||||
'doctype': 'DocPerm',
|
||||
'idx': 1,
|
||||
'name': '__common__',
|
||||
'parent': 'Email Digest',
|
||||
'parentfield': 'permissions',
|
||||
'parenttype': 'DocType',
|
||||
'permlevel': 0,
|
||||
'read': 1,
|
||||
'role': 'Administrator',
|
||||
'write': 1
|
||||
'role': 'System Manager'
|
||||
},
|
||||
|
||||
# DocType, Email Digest
|
||||
@@ -56,34 +52,290 @@
|
||||
|
||||
# DocPerm
|
||||
{
|
||||
'doctype': 'DocPerm'
|
||||
'cancel': 1,
|
||||
'create': 1,
|
||||
'doctype': 'DocPerm',
|
||||
'permlevel': 0,
|
||||
'write': 1
|
||||
},
|
||||
|
||||
# DocPerm
|
||||
{
|
||||
'doctype': 'DocPerm',
|
||||
'permlevel': 1
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'doctype': 'DocField',
|
||||
'fieldtype': 'HTML',
|
||||
'idx': 1,
|
||||
'label': 'Body'
|
||||
'fieldtype': 'Section Break',
|
||||
'label': 'Settings',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'content_config',
|
||||
'fieldtype': 'Text',
|
||||
'fieldtype': 'Column Break',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'enabled',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Enabled',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'company',
|
||||
'fieldtype': 'Select',
|
||||
'label': 'For Company',
|
||||
'options': 'link:Company',
|
||||
'permlevel': 0,
|
||||
'reqd': 1
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'allow_on_submit': 0,
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'frequency',
|
||||
'fieldtype': 'Select',
|
||||
'label': 'How frequently?',
|
||||
'options': '\nDaily\nWeekly\nMonthly',
|
||||
'permlevel': 0,
|
||||
'reqd': 1
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.enabled',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'next_send',
|
||||
'fieldtype': 'Data',
|
||||
'label': 'Next email will be sent on:',
|
||||
'permlevel': 1
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'default': '1',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'use_standard',
|
||||
'fieldtype': 'Check',
|
||||
'hidden': 1,
|
||||
'idx': 2,
|
||||
'label': 'Content Config'
|
||||
'label': 'Use standard?',
|
||||
'permlevel': 0,
|
||||
'search_index': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'email_config',
|
||||
'fieldtype': 'Column Break',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'doctype': 'DocField',
|
||||
'fieldtype': 'Button',
|
||||
'label': 'Add/Remove Recipients',
|
||||
'permlevel': 0,
|
||||
'trigger': 'Client'
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'description': 'Note: Email will not be sent to disabled users',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'recipient_list',
|
||||
'fieldtype': 'Text',
|
||||
'label': 'Recipients',
|
||||
'permlevel': 1,
|
||||
'reqd': 1
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldtype': 'Section Break',
|
||||
'label': 'Select Digest Content',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'new_leads',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'New Leads',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'new_enquiries',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'New Enquiries',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'new_quotations',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'New Quotations',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'new_sales_orders',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'New Sales Orders',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'new_purchase_orders',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'New Purchase Orders',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'new_transactions',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'New Transactions',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'payables',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Payables',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'payments',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Payments',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'expenses_booked',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Expenses Booked',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'invoiced_amount',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Invoiced Amount (Receivables)',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'collections',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Collections',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'income',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Income',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'bank_balance',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Bank Balance',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'stock_below_rl',
|
||||
'fieldtype': 'Check',
|
||||
'hidden': 1,
|
||||
'idx': 3,
|
||||
'label': 'Email Config'
|
||||
'label': 'Stock Items below re-order level',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:!doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldtype': 'Section Break',
|
||||
'label': 'Enter Custom Code',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:!doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'custom_code',
|
||||
'fieldtype': 'Code',
|
||||
'label': 'Custom Python Code',
|
||||
'permlevel': 0
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'depends_on': 'eval:!doc.use_standard',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'email_template',
|
||||
'fieldtype': 'Code',
|
||||
'label': 'Email Template',
|
||||
'permlevel': 0
|
||||
}
|
||||
]
|
||||
@@ -8,7 +8,6 @@ class DocType:
|
||||
"""
|
||||
from webnotes.model import default_fields
|
||||
from webnotes.utils import set_default
|
||||
|
||||
for key in self.doc.fields:
|
||||
if key not in default_fields:
|
||||
set_default(key, self.doc.fields[key])
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
{
|
||||
'creation': '2011-09-07 11:59:05',
|
||||
'docstatus': 0,
|
||||
'modified': '2011-12-06 18:48:53',
|
||||
'modified': '2011-12-14 11:05:55',
|
||||
'modified_by': 'Administrator',
|
||||
'owner': 'Administrator'
|
||||
},
|
||||
|
||||
# These values are common for all DocType
|
||||
{
|
||||
'_last_update': '1323176623',
|
||||
'_last_update': '1323840127',
|
||||
'colour': 'White:FFF',
|
||||
'default_print_format': 'Standard',
|
||||
'doctype': 'DocType',
|
||||
@@ -22,7 +22,7 @@
|
||||
'name_case': 'Title Case',
|
||||
'section_style': 'Simple',
|
||||
'show_in_menu': 1,
|
||||
'version': 24
|
||||
'version': 26
|
||||
},
|
||||
|
||||
# These values are common for all DocField
|
||||
@@ -179,7 +179,17 @@
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'fs_discounts',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Discounts'
|
||||
'label': 'Sales Discounts'
|
||||
},
|
||||
|
||||
# DocField
|
||||
{
|
||||
'colour': 'White:FFF',
|
||||
'description': 'Discount Fields will be available in Purchase Order, Purchase Receipt, Purchase Invoice',
|
||||
'doctype': 'DocField',
|
||||
'fieldname': 'fs_purchase_discounts',
|
||||
'fieldtype': 'Check',
|
||||
'label': 'Purchase Discounts'
|
||||
},
|
||||
|
||||
# DocField
|
||||
|
||||
@@ -14,7 +14,8 @@ class DocType:
|
||||
# on update
|
||||
#
|
||||
def validate(self):
|
||||
if self.doc.file_list and self.doc.set_from_attachment:
|
||||
from webnotes.utils import cint
|
||||
if self.doc.file_list and cint(self.doc.set_from_attachment):
|
||||
self.set_html_from_image()
|
||||
|
||||
# update control panel - so it loads new letter directly
|
||||
|
||||
@@ -161,6 +161,7 @@ SetupData = function(cnty){
|
||||
['Custom Field',1,'Custom Field','dt'+NEWLINE+'label'+NEWLINE+'fieldtype'+NEWLINE+'options','Add and manage custom fields on forms'],
|
||||
['Email Settings',3,'Email Settings','','Outgoing email server and address'],
|
||||
['Notification Settings',3,'Notification Control','','Automatic emails set at selected events'],
|
||||
['Email Digest', 1, 'Email Digest', '', 'Schedule Daily / Weekly / Monthly Summary e-mails'],
|
||||
['Company',1,'Company','id'+NEWLINE+'is_active'+NEWLINE+'email','Manage list of companies'],
|
||||
['Fiscal Year',1,'Fiscal Year','id'+NEWLINE+'company'+NEWLINE+'is_active'+NEWLINE+'year','Manage list of fiscal years'],
|
||||
['Personalize',3,'Personalize','','Set your banner'],
|
||||
@@ -168,7 +169,7 @@ SetupData = function(cnty){
|
||||
['Import Data',2,'Import Data','','Import data from CSV files'],
|
||||
['Manage Users',2,'My Company','','Add / remove users and manage their roles'],
|
||||
['Web Forms',2,'Webforms','', 'Code to embed forms in yor website'],
|
||||
['Permissions Manager',2,'Permission Engine','', 'Manage all permissions from one tool (beta)'],
|
||||
['Permissions Manager',2,'Permission Engine','', 'Manage all permissions from one tool'],
|
||||
//['Property Setter',1,'Property Setter','', 'Customize properties of a Form (DocType) or Field'],
|
||||
['Customize Form View',3,'DocLayer','', 'Customize properties of a Form (DocType) or Field'],
|
||||
['Print Formats', 1, 'Print Format', '', 'Manage Print Formats'],
|
||||
|
||||
Reference in New Issue
Block a user