diff --git a/frappe/__init__.py b/frappe/__init__.py index 89077e4b59..dc524cd908 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '12.0.14' +__version__ = '12.0.15' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index 85986dd9d5..7bc13accf8 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -21,6 +21,7 @@ "designation", "gender", "phone", + "mobile_no", "company_name", "image", "sb_00", @@ -192,9 +193,15 @@ { "fieldname": "phone_nos", "fieldtype": "Table", - "label": "Phone Nos", + "label": "Numbers", "options": "Contact Phone" }, + { + "fieldname": "mobile_no", + "fieldtype": "Data", + "label": "Mobile No", + "read_only": 1 + }, { "default": "0", "fieldname": "pulled_from_google_contacts", @@ -238,8 +245,8 @@ "icon": "fa fa-user", "idx": 1, "image_field": "image", - "modified": "2019-09-13 15:50:38.999884", - "modified_by": "himanshu@erpnext.com", + "modified": "2019-09-24 17:48:26.790985", + "modified_by": "Administrator", "module": "Contacts", "name": "Contact", "name_case": "Title Case", diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 0b1231e511..f851e19b46 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -29,11 +29,10 @@ class Contact(Document): break def validate(self): - self.set_primary("email_id", "email_ids") - self.set_primary("phone", "phone_nos") - - if self.email_id: - self.email_id = self.email_id.strip() + self.set_primary_email() + self.set_primary("phone") + self.set_primary("mobile_no") + self.check_if_primary_phone_and_mobile_no_same() self.set_user() @@ -79,24 +78,51 @@ class Contact(Document): if autosave: self.save(ignore_permissions=True) - def add_phone(self, phone, is_primary=0, autosave=False): + def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): self.append("phone_nos", { "phone": phone, - "is_primary": is_primary + "is_primary_phone": is_primary_phone, + "is_primary_mobile_no": is_primary_mobile_no }) if autosave: self.save(ignore_permissions=True) - def set_primary(self, fieldname, child_table): - if len(self.get(child_table)) == 1: - self.get(child_table)[0].is_primary = 1 - setattr(self, fieldname, self.get(child_table)[0].get(fieldname)) - else: - for d in self.get(child_table): - if d.is_primary == 1: - setattr(self, fieldname, d.get(fieldname)) - break + def set_primary_email(self): + if not self.email_ids: + self.email_id = "" + return + + if len([email.email_id for email in self.email_ids if email.is_primary]) > 1: + frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Email ID"))) + + for d in self.email_ids: + if d.is_primary == 1: + self.email_id = d.email_id.strip() + break + + def set_primary(self, fieldname): + # Used to set primary mobile and phone no. + if len(self.phone_nos) == 0: + setattr(self, fieldname, "") + return + + field_name = "is_primary_" + fieldname + + is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)] + + if len(is_primary) > 1: + frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname)))) + + for d in self.phone_nos: + if d.get(field_name) == 1: + setattr(self, fieldname, d.phone) + break + + def check_if_primary_phone_and_mobile_no_same(self): + if self.phone and self.mobile_no and self.phone == self.mobile_no: + number = frappe.bold(self.phone) + frappe.throw(_("Number {0} cannot be set as primary for Phone as well as Mobile No.").format(number)) def get_default_contact(doctype, name): '''Returns default contact for the given doctype, name''' diff --git a/frappe/contacts/doctype/contact/test_contact.py b/frappe/contacts/doctype/contact/test_contact.py index 496ff68299..2e6f9ad422 100644 --- a/frappe/contacts/doctype/contact/test_contact.py +++ b/frappe/contacts/doctype/contact/test_contact.py @@ -5,8 +5,82 @@ from __future__ import unicode_literals import frappe import unittest - -test_records = frappe.get_test_records('Contact') +from frappe.exceptions import ValidationError class TestContact(unittest.TestCase): - pass + + def test_check_default_email(self): + emails = [ + {"email": "test1@example.com", "is_primary": 0}, + {"email": "test2@example.com", "is_primary": 0}, + {"email": "test3@example.com", "is_primary": 0}, + {"email": "test4@example.com", "is_primary": 1}, + {"email": "test5@example.com", "is_primary": 0}, + ] + contact = create_contact("Email", "Mr", emails=emails) + + self.assertEqual(contact.email_id, "test4@example.com") + + def test_check_default_phone_and_mobile(self): + phones = [ + {"phone": "+91 0000000000", "is_primary_phone": 0, "is_primary_mobile_no": 0}, + {"phone": "+91 0000000001", "is_primary_phone": 0, "is_primary_mobile_no": 0}, + {"phone": "+91 0000000002", "is_primary_phone": 1, "is_primary_mobile_no": 0}, + {"phone": "+91 0000000003", "is_primary_phone": 0, "is_primary_mobile_no": 1}, + ] + contact = create_contact("Phone", "Mr", phones=phones) + + self.assertEqual(contact.phone, "+91 0000000002") + self.assertEqual(contact.mobile_no, "+91 0000000003") + + def test_same_phone_and_mobile(self): + phones = [ + {"phone": "+91 0000000000", "is_primary_phone": 1, "is_primary_mobile_no": 1}, + ] + contact = create_contact("Phone", "Mr", phones=phones, save=False) + self.assertRaises(ValidationError, contact.save) + + def test_no_primary_set(self): + emails = [ + {"email": "test1@example.com", "is_primary": 0}, + {"email": "test2@example.com", "is_primary": 0}, + {"email": "test3@example.com", "is_primary": 0}, + {"email": "test4@example.com", "is_primary": 0}, + {"email": "test5@example.com", "is_primary": 0}, + ] + phones = [ + {"phone": "+91 0000000000", "is_primary_phone": 0, "is_primary_mobile_no": 0}, + {"phone": "+91 0000000001", "is_primary_phone": 0, "is_primary_mobile_no": 0}, + {"phone": "+91 0000000002", "is_primary_phone": 1, "is_primary_mobile_no": 1}, + {"phone": "+91 0000000003", "is_primary_phone": 0, "is_primary_mobile_no": 0}, + ] + + contact_email = create_contact("Default", "Mr", emails=emails, phones=phones, save=False) + contact_phone = create_contact("Default", "Mr", emails=emails, phones=phones, save=False) + + # No default set for emails if many emails are passed as params + self.assertRaises(ValidationError, contact_email.save) + + # No default set for phones if many phones are passed as params + self.assertRaises(ValidationError, contact_phone.save) + +def create_contact(name, salutation, emails=None, phones=None, save=True): + doc = frappe.get_doc({ + "doctype": "Contact", + "first_name": name, + "status": "Open", + "salutation": salutation + }) + + if emails: + for d in emails: + doc.add_email(d.get("email"), d.get("is_primary")) + + if phones: + for d in phones: + doc.add_phone(d.get("phone"), d.get("is_primary_phone"), d.get("is_primary_mobile_no")) + + if save: + doc.insert() + + return doc \ No newline at end of file diff --git a/frappe/contacts/doctype/contact/test_records.json b/frappe/contacts/doctype/contact/test_records.json index 8f4be113b4..11c5329e93 100644 --- a/frappe/contacts/doctype/contact/test_records.json +++ b/frappe/contacts/doctype/contact/test_records.json @@ -1,19 +1,39 @@ [ - { - "doctype": "Contact", - "salutation": "Mr", - "email_id": "test_contact@example.com", - "first_name": "_Test Contact For _Test Customer", - "is_primary_contact": 1, - "phone": "+91 0000000000", - "status": "Open" - }, - { - "doctype": "Contact", - "email_id": "test_contact@example.com", - "first_name": "_Test Contact For _Test Supplier", - "is_primary_contact": 1, - "phone": "+91 0000000000", - "status": "Open" - } + { + "doctype": "Contact", + "salutation": "Mr", + "first_name": "_Test Contact For _Test Customer", + "is_primary_contact": 1, + "status": "Open", + "email_ids": [ + { + "email_id": "test_contact@example.com", + "is_primary": 1 + } + ], + "phone_nos": [ + { + "phone": "+91 0000000000", + "is_primary_phone": 1 + } + ] + }, + { + "doctype": "Contact", + "first_name": "_Test Contact For _Test Supplier", + "is_primary_contact": 1, + "status": "Open", + "email_ids": [ + { + "email_id": "test_contact@example.com", + "is_primary": 1 + } + ], + "phone_nos": [ + { + "phone": "+91 0000000000", + "is_primary_phone": 1 + } + ] + } ] \ No newline at end of file diff --git a/frappe/contacts/doctype/contact_email/contact_email.json b/frappe/contacts/doctype/contact_email/contact_email.json index cfcc39ca8b..f36e155a75 100644 --- a/frappe/contacts/doctype/contact_email/contact_email.json +++ b/frappe/contacts/doctype/contact_email/contact_email.json @@ -13,9 +13,11 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Email ID", - "options": "Email" + "options": "Email", + "reqd": 1 }, { + "columns": 2, "default": "0", "fieldname": "is_primary", "fieldtype": "Check", @@ -24,7 +26,7 @@ } ], "istable": 1, - "modified": "2019-08-02 13:14:22.193463", + "modified": "2019-09-24 17:47:30.565805", "modified_by": "Administrator", "module": "Contacts", "name": "Contact Email", diff --git a/frappe/contacts/doctype/contact_phone/contact_phone.json b/frappe/contacts/doctype/contact_phone/contact_phone.json index 971dedf3d2..3fb203ed69 100644 --- a/frappe/contacts/doctype/contact_phone/contact_phone.json +++ b/frappe/contacts/doctype/contact_phone/contact_phone.json @@ -5,25 +5,36 @@ "engine": "InnoDB", "field_order": [ "phone", - "is_primary" + "is_primary_phone", + "is_primary_mobile_no" ], "fields": [ - { - "default": "0", - "fieldname": "is_primary", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Is Primary" - }, { "fieldname": "phone", "fieldtype": "Data", "in_list_view": 1, - "label": "Phone" + "label": "Number", + "reqd": 1 + }, + { + "columns": 2, + "default": "0", + "fieldname": "is_primary_phone", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Primary Phone" + }, + { + "columns": 2, + "default": "0", + "fieldname": "is_primary_mobile_no", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Primary Mobile" } ], "istable": 1, - "modified": "2019-08-05 11:40:59.104224", + "modified": "2019-09-24 17:47:50.375326", "modified_by": "Administrator", "module": "Contacts", "name": "Contact Phone", diff --git a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py index fb345a4384..ba0ecd41d1 100644 --- a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py +++ b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py @@ -7,7 +7,7 @@ import frappe from frappe import _ field_map = { - "Contact": ["first_name", "last_name", "address", "phone", "email_id", "is_primary_contact"], + "Contact": ["first_name", "last_name", "address", "phone", "mobile_no", "email_id", "is_primary_contact"], "Address": ["address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address"] } diff --git a/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py index d87676e272..2db395102a 100644 --- a/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py +++ b/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py @@ -82,8 +82,8 @@ def create_linked_contact(link_list, address): "address": address, "status": "Open" }) - contact.add_email("test_contact@example.com") - contact.add_phone("+91 0000000000") + contact.add_email("test_contact@example.com", is_primary=True) + contact.add_phone("+91 0000000000", is_primary_phone=True) for name in link_list: contact.append("links",{ @@ -103,7 +103,7 @@ class TestAddressesAndContacts(unittest.TestCase): create_linked_contact(links_list, d) report_data = get_data({"reference_doctype": "Test Custom Doctype"}) for idx, link in enumerate(links_list): - test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', 'test_contact@example.com', 1] + test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', None, 'test_contact@example.com', 1] self.assertListEqual(test_item, report_data[idx]) def tearDown(self): diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index dcbf3ee35a..6f15e69d30 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1027,9 +1027,10 @@ def update_roles(role_profile): user.add_roles(*roles) def create_contact(user, ignore_links=False, ignore_mandatory=False): + from frappe.contacts.doctype.contact.contact import get_contact_name if user.name in ["Administrator", "Guest"]: return - if not frappe.db.get_value("Contact", {"email_id": user.email}): + if not get_contact_name(user.email): contact = frappe.get_doc({ "doctype": "Contact", "first_name": user.first_name, @@ -1039,7 +1040,7 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False): }) if user.email: - contact.add_email(user.email) + contact.add_email(user.email, is_primary=True) if user.phone: contact.add_phone(user.phone) @@ -1048,7 +1049,6 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False): contact.add_phone(user.mobile_no) contact.insert(ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory) - @frappe.whitelist() def generate_keys(user): """ diff --git a/frappe/database/database.py b/frappe/database/database.py index 412051c76f..a1b8d390a9 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -607,7 +607,7 @@ class Database(object): """Update multiple values. Alias for `set_value`.""" return self.set_value(*args, **kwargs) - def set_value(self, dt, dn, field, val, modified=None, modified_by=None, + def set_value(self, dt, dn, field, val=None, modified=None, modified_by=None, update_modified=True, debug=False): """Set a single value in the database, do not call the ORM triggers but update the modified timestamp (unless specified not to). diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index a82ff44974..637904b35c 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -52,7 +52,7 @@ class Event(Document): ["Communication Link", "link_doctype", "=", participant.reference_doctype], ["Communication Link", "link_name", "=", participant.reference_docname] ] - comms = frappe.get_list("Communication", filters=filters, fields=["name"]) + comms = frappe.get_all("Communication", filters=filters, fields=["name"]) if comms: for comm in comms: diff --git a/frappe/hooks.py b/frappe/hooks.py index 39694928d6..d92b806211 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -270,7 +270,7 @@ user_privacy_documents = [ { 'doctype': 'Contact', 'match_field': 'email_id', - 'personal_fields': ['first_name', 'last_name', 'phone'], + 'personal_fields': ['first_name', 'last_name', 'phone', 'mobile_no'], }, { 'doctype': 'Contact Email', diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 738c097f63..8c6ab97017 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -187,7 +187,7 @@ def sync_contacts_from_google_contacts(g_contact): contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("metadata").get("primary") else 0) for phone in connection.get("phoneNumbers", []): - contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("metadata").get("primary") else 0) + contact.add_phone(phone=phone.get("value"), is_primary_phone=1 if phone.get("metadata").get("primary") else 0) contact.insert(ignore_permissions=True) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 5676f7deaa..3d5b002c75 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -457,9 +457,15 @@ class BaseDocument(object): meta = frappe.get_meta(doctype) if meta.has_field('disabled'): - disabled = frappe.get_value(doctype, self.get(df.fieldname), 'disabled') - if disabled: - frappe.throw(_("{0} is disabled").format(frappe.bold(self.get(df.fieldname)))) + if not ( + frappe.flags.in_import + or frappe.flags.in_migrate + or frappe.flags.in_install + or frappe.flags.in_patch + ): + disabled = frappe.get_value(doctype, self.get(df.fieldname), 'disabled') + if disabled: + frappe.throw(_("{0} is disabled").format(frappe.bold(self.get(df.fieldname)))) else: doctype = self.get(df.options) if not doctype: diff --git a/frappe/model/document.py b/frappe/model/document.py index fd1ffec483..f93c366ffb 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1232,6 +1232,18 @@ class Document(BaseDocument): frappe.bold(self.meta.get_label(from_date_field)), ), frappe.exceptions.InvalidDates) + def get_assigned_users(self): + assignments = frappe.get_all('ToDo', + fields=['owner'], + filters={ + 'reference_type': self.doctype, + 'reference_name': self.name, + 'status': ('!=', 'Cancelled'), + }) + + users = set([assignment.owner for assignment in assignments]) + return users + def execute_action(doctype, name, action, **kwargs): '''Execute an action on a document (called by background worker)''' doc = frappe.get_doc(doctype, name) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index b06297023e..bba2f62856 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -119,7 +119,7 @@ def sync_customizations_for_doctype(data, folder): custom_doctype, doctype_fieldname), doc_type) for d in data[key]: - _insert(data) + _insert(d) else: for d in data[key]: @@ -242,7 +242,7 @@ def make_boilerplate(template, doc, opts=None): base_class = 'Document' base_class_import = 'from frappe.model.document import Document' - if doc.is_tree: + if doc.get('is_tree'): base_class = 'NestedSet' base_class_import = 'from frappe.utils.nestedset import NestedSet' diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 71287f10c0..4052d2debb 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -117,18 +117,20 @@ frappe.ui.form.Form = class FrappeForm { add_nav_keyboard_shortcuts() { frappe.ui.keys.add_shortcut({ - shortcut: 'shift+>', + shortcut: 'shift+ctrl+>', action: () => this.navigate_records(0), page: this.page, description: __('Go to next record'), + ignore_inputs: true, condition: () => !this.is_new() }); frappe.ui.keys.add_shortcut({ - shortcut: 'shift+<', + shortcut: 'shift+ctrl+<', action: () => this.navigate_records(1), page: this.page, description: __('Go to previous record'), + ignore_inputs: true, condition: () => !this.is_new() }); } @@ -837,10 +839,16 @@ frappe.ui.form.Form = class FrappeForm { frappe.call('frappe.desk.form.utils.get_next', args).then(r => { if (r.message) { frappe.set_route('Form', this.doctype, r.message); + this.focus_on_first_input(); } }); } + focus_on_first_input() { + let $first_input_el = $(frappe.container.page).find('.frappe-control:visible').eq(0); + $first_input_el.find('input, select, textarea').focus(); + } + rename_doc() { frappe.model.rename_doc(this.doctype, this.docname, () => this.refresh_header()); } diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index 2baeb6d033..511b0468c5 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -8,12 +8,26 @@ frappe.ui.FilterGroup = class { make() { this.wrapper.append(this.get_container_template()); + this.toggle_clear_filter(); this.set_events(); } + toggle_clear_filter() { + let clear_filter_button = this.wrapper.find('.remove-filters'); + if (this.filters.length == 0) { + clear_filter_button.hide(); + } else { + clear_filter_button.show(); + } + } set_events() { this.wrapper.find('.add-filter').on('click', () => { - this.add_filter(this.doctype, 'name'); + this.add_filter(this.doctype, 'name') + .then(this.toggle_clear_filter()); + + }); + this.wrapper.find('.remove-filters').on('click', () => { + this.clear_filters(); }); } @@ -34,7 +48,6 @@ frappe.ui.FilterGroup = class { // {}: Add in page filter by fieldname if exists ('=' => 'like') if(!this.validate_args(doctype, fieldname)) return false; - const is_new_filter = arguments.length < 2; if (is_new_filter && this.wrapper.find(".new-filter:visible").length) { // only allow 1 new filter at a time! @@ -123,6 +136,7 @@ frappe.ui.FilterGroup = class { update_filters() { this.filters = this.filters.filter(f => f.field); // remove hidden filters + this.toggle_clear_filter(); } clear_filters() { @@ -140,9 +154,12 @@ frappe.ui.FilterGroup = class { get_container_template() { return $(`
- +
`); diff --git a/frappe/public/js/frappe/views/components/Modules.vue b/frappe/public/js/frappe/views/components/Modules.vue index a746902b53..fd5f11d21f 100644 --- a/frappe/public/js/frappe/views/components/Modules.vue +++ b/frappe/public/js/frappe/views/components/Modules.vue @@ -41,7 +41,7 @@ export default { methods: { update_current_module() { let route = frappe.get_route() - if (route[0] === 'modules' || !route[0]) { + if (route[0] === 'modules') { this.route = route let module = this.modules_list.filter(m => m.module_name == route[1])[0] let module_name = module && (module.label || module.module_name) diff --git a/frappe/public/less/filters.less b/frappe/public/less/filters.less index cf25e0599d..30894eea19 100644 --- a/frappe/public/less/filters.less +++ b/frappe/public/less/filters.less @@ -1,9 +1,17 @@ @import 'common'; .active-tag-filters { - .add-filter, .filter-tag { + display: flex; + overflow: scroll; + .filter-button, .filter-tag { margin: 0 10px 10px 0; } + .add-filter { + align-self: flex-start; + } + .remove-filters { + margin-left: auto; + } } .toggle-filter { @@ -47,7 +55,6 @@ .filter-box .row > div[class*="col-sm-"] { padding-right: 0px; } - .filter-field { width: 65% !important; diff --git a/frappe/social/doctype/energy_point_log/test_energy_point_log.py b/frappe/social/doctype/energy_point_log/test_energy_point_log.py index c44b3b14df..14c5a8e3d8 100644 --- a/frappe/social/doctype/energy_point_log/test_energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/test_energy_point_log.py @@ -7,6 +7,7 @@ import frappe import unittest from .energy_point_log import get_energy_points as _get_energy_points, create_review_points_log, review from frappe.utils.testutils import add_custom_field, clear_custom_fields +from frappe.desk.form.assign_to import add as assign_to class TestEnergyPointLog(unittest.TestCase): def tearDown(self): @@ -185,16 +186,53 @@ class TestEnergyPointLog(unittest.TestCase): self.assertEquals(points_after_todo_creation, points_before_todo_creation + todo_point_rule.points) -def create_energy_point_rule_for_todo(multiplier_field=None, for_doc_event='Custom', max_points=None): - name = 'ToDo Closed' - point_rule = frappe.db.get_all( - 'Energy Point Rule', - {'name': name}, - ['*'], - limit=1 - ) + def test_point_allocation_for_assigned_users(self): + todo = create_a_todo() - if point_rule: return point_rule[0] + assign_users_to_todo(todo.name, ['test@example.com', 'test2@example.com']) + + test_user_before_points = get_points('test@example.com') + test2_user_before_points = get_points('test2@example.com') + + rule = create_energy_point_rule_for_todo(for_assigned_users=1) + + todo.status = 'Closed' + todo.save() + + test_user_after_points = get_points('test@example.com') + test2_user_after_points = get_points('test2@example.com') + + self.assertEquals(test_user_after_points, + test_user_before_points + rule.points) + + self.assertEquals(test2_user_after_points, + test2_user_before_points + rule.points) + + def test_points_on_field_value_change(self): + rule = create_energy_point_rule_for_todo(for_doc_event='Value Change', + field_to_check='description') + + frappe.set_user('test@example.com') + points_before_todo_creation = get_points('test@example.com') + todo = create_a_todo() + todo.status = 'Closed' + todo.save() + points_after_closing_todo = get_points('test@example.com') + self.assertEquals(points_after_closing_todo, + points_before_todo_creation) + + todo.description = 'This is new todo' + todo.save() + points_after_changing_todo_description = get_points('test@example.com') + self.assertEquals(points_after_changing_todo_description, + points_before_todo_creation + rule.points) + +def create_energy_point_rule_for_todo(multiplier_field=None, for_doc_event='Custom', + max_points=None, for_assigned_users=0, field_to_check=None): + name = 'ToDo Closed' + point_rule_exists = frappe.db.exists('Energy Point Rule', name) + + if point_rule_exists: return frappe.get_doc('Energy Point Rule', name) return frappe.get_doc({ 'doctype': 'Energy Point Rule', @@ -204,8 +242,10 @@ def create_energy_point_rule_for_todo(multiplier_field=None, for_doc_event='Cust 'condition': 'doc.status == "Closed"', 'for_doc_event': for_doc_event, 'user_field': 'owner', + 'for_assigned_users': for_assigned_users, 'multiplier_field': multiplier_field, - 'max_points': max_points + 'max_points': max_points, + 'field_to_check': field_to_check }).insert(ignore_permissions=1) def create_a_todo(): @@ -216,4 +256,12 @@ def create_a_todo(): def get_points(user, point_type='energy_points'): - return _get_energy_points(user).get(point_type) or 0 \ No newline at end of file + return _get_energy_points(user).get(point_type) or 0 + +def assign_users_to_todo(todo_name, users): + for user in users: + assign_to({ + 'assign_to': user, + 'doctype': 'ToDo', + 'name': todo_name + }) \ No newline at end of file diff --git a/frappe/social/doctype/energy_point_rule/energy_point_rule.js b/frappe/social/doctype/energy_point_rule/energy_point_rule.js index e8c338d162..6355b889aa 100644 --- a/frappe/social/doctype/energy_point_rule/energy_point_rule.js +++ b/frappe/social/doctype/energy_point_rule/energy_point_rule.js @@ -2,19 +2,34 @@ // For license information, please see license.txt frappe.ui.form.on('Energy Point Rule', { - refresh: function(frm) { - frm.events.set_user_and_multiplier_field_options(frm); + validate(frm) { + frm.set_df_property('user_field', 'reqd', !frm.doc.for_assigned_users); + frm.set_df_property('condition', 'reqd', frm.doc.for_doc_event==='Custom'); + }, + refresh(frm) { + frm.events.set_field_options(frm); + }, + for_doc_event(frm) { + if (frm.doc.for_assigned_users) { + frm.set_value('for_assigned_users', !frm.doc.for_doc_event==='New'); + } }, reference_doctype(frm) { - frm.events.set_user_and_multiplier_field_options(frm); + frm.events.set_field_options(frm); }, - set_user_and_multiplier_field_options(frm) { + set_field_options(frm) { + // sets options for field_to_check, user_field and multiplier fields + // based on reference doctype const reference_doctype = frm.doc.reference_doctype; if (!reference_doctype) return; frappe.model.with_doctype(reference_doctype, () => { const map_for_options = df => ({ label: df.label, value: df.fieldname }); - const fields = frappe.meta.get_docfields(frm.doc.reference_doctype); + const fields = frappe.meta.get_docfields(frm.doc.reference_doctype) + .filter(frappe.model.is_value_type); + + const fields_to_check = fields.map(map_for_options); + const user_fields = fields.filter(df => (df.fieldtype === 'Link' && df.options === 'User') || df.fieldtype === 'Data') .map(map_for_options) @@ -29,6 +44,7 @@ frappe.ui.form.on('Energy Point Rule', { // blank option for the ability to unset the multiplier field multiplier_fields.unshift(null); + frm.set_df_property('field_to_check', 'options', fields_to_check); frm.set_df_property('user_field', 'options', user_fields); frm.set_df_property('multiplier_field', 'options', multiplier_fields); }); diff --git a/frappe/social/doctype/energy_point_rule/energy_point_rule.json b/frappe/social/doctype/energy_point_rule/energy_point_rule.json index 7a599701dc..d008237862 100644 --- a/frappe/social/doctype/energy_point_rule/energy_point_rule.json +++ b/frappe/social/doctype/energy_point_rule/energy_point_rule.json @@ -10,11 +10,14 @@ "rule_name", "reference_doctype", "for_doc_event", - "condition", + "field_to_check", "points", + "for_assigned_users", "user_field", "multiplier_field", - "max_points" + "max_points", + "column_break_12", + "condition" ], "fields": [ { @@ -42,7 +45,7 @@ "reqd": 1 }, { - "depends_on": "eval:doc.for_doc_event === 'Custom'", + "depends_on": "eval:['Custom', 'Value Change'].includes(doc.for_doc_event)", "description": "If the condition is satisfied user will be rewarded with the points. eg. doc.status == 'Closed'\n", "fieldname": "condition", "fieldtype": "Code", @@ -56,11 +59,11 @@ "reqd": 1 }, { + "depends_on": "eval:!doc.for_assigned_users || doc.for_doc_event==='New'", "description": "The user from this field will be rewarded points", "fieldname": "user_field", "fieldtype": "Select", - "label": "User Field", - "reqd": 1 + "label": "User Field" }, { "fieldname": "multiplier_field", @@ -69,7 +72,7 @@ }, { "depends_on": "eval:doc.multiplier_field", - "description": "Maximum points allowed after multiplying points with the multiplier value\n(Note: For no limit set value as 0)", + "description": "Maximum points allowed after multiplying points with the multiplier value\n(Note: For no limit leave this field empty or set 0)", "fieldname": "max_points", "fieldtype": "Int", "label": "Maximum Points" @@ -83,10 +86,28 @@ "fieldname": "for_doc_event", "fieldtype": "Select", "label": "For Document Event", - "options": "New\nSubmit\nCancel\nCustom" + "options": "New\nSubmit\nCancel\nValue Change\nCustom" + }, + { + "default": "0", + "depends_on": "eval:doc.for_doc_event !=='New'", + "description": "Users assigned to the reference document will get points.", + "fieldname": "for_assigned_users", + "fieldtype": "Check", + "label": "Allot Points To Assigned Users" + }, + { + "depends_on": "eval:doc.for_doc_event=='Value Change'", + "fieldname": "field_to_check", + "fieldtype": "Select", + "label": "Field To Check" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" } ], - "modified": "2019-09-05 14:22:27.664645", + "modified": "2019-09-24 15:20:25.383536", "modified_by": "Administrator", "module": "Social", "name": "Energy Point Rule", diff --git a/frappe/social/doctype/energy_point_rule/energy_point_rule.py b/frappe/social/doctype/energy_point_rule/energy_point_rule.py index 1641e05f5e..2ca2ffa44e 100644 --- a/frappe/social/doctype/energy_point_rule/energy_point_rule.py +++ b/frappe/social/doctype/energy_point_rule/energy_point_rule.py @@ -31,21 +31,24 @@ class EnergyPointRule(Document): reference_doctype = doc.doctype reference_name = doc.name - user = doc.get(self.user_field) + users = [] + if self.for_assigned_users: + users = doc.get_assigned_users() + else: + users = [doc.get(self.user_field)] rule = self.name # incase of zero as result after roundoff if not points: return - # if user_field has no value - if not user or user == 'Administrator': return - try: - create_energy_points_log(reference_doctype, reference_name, { - 'points': points, - 'user': user, - 'rule': rule - }) + for user in users: + if not user or user == 'Administrator': continue + create_energy_points_log(reference_doctype, reference_name, { + 'points': points, + 'user': user, + 'rule': rule + }) except Exception as e: frappe.log_error(frappe.get_traceback(), 'apply_energy_point') @@ -57,10 +60,25 @@ class EnergyPointRule(Document): return doc.docstatus == 1 if self.for_doc_event == 'Cancel': return doc.docstatus == 2 + if self.for_doc_event == 'Value Change': + field_to_check = self.field_to_check + if not field_to_check: return False + doc_before_save = doc.get_doc_before_save() + # check if the field has been changed + # if condition is set check if it is satisfied + return doc_before_save \ + and doc_before_save.get(field_to_check) != doc.get(field_to_check) \ + and (not self.condition or self.eval_condition(doc)) + if self.for_doc_event == 'Custom' and self.condition: - return frappe.safe_eval(self.condition, None, {'doc': doc.as_dict()}) + return self.eval_condition(doc) return False + def eval_condition(self, doc): + return self.condition and frappe.safe_eval(self.condition, None, { + 'doc': doc.as_dict() + }) + def process_energy_points(doc, state): if (frappe.flags.in_patch or frappe.flags.in_install diff --git a/frappe/social/doctype/energy_point_settings/energy_point_settings.py b/frappe/social/doctype/energy_point_settings/energy_point_settings.py index 65b8a2626c..737aab587c 100644 --- a/frappe/social/doctype/energy_point_settings/energy_point_settings.py +++ b/frappe/social/doctype/energy_point_settings/energy_point_settings.py @@ -21,17 +21,21 @@ def allocate_review_points(): settings.point_allocation_periodicity): return + user_point_map = {} + for level in settings.review_levels: - create_review_points(level) + users = get_users_with_role(level.role) + for user in users: + user_point_map.setdefault(user, 0) + # to avoid duplicate point allocation + user_point_map[user] = max([user_point_map[user], level.review_points]) + + for user, points in user_point_map.items(): + create_review_points_log(user, points) settings.last_point_allocation_date = today() settings.save(ignore_permissions=True) -def create_review_points(level): - users = get_users_with_role(level.role) - for user in users: - create_review_points_log(user, level.review_points) - def can_allocate_today(last_date, periodicity): if not last_date: return True diff --git a/frappe/website/doctype/personal_data_download_request/test_personal_data_download_request.py b/frappe/website/doctype/personal_data_download_request/test_personal_data_download_request.py index 64d4e45660..9a62b845e1 100644 --- a/frappe/website/doctype/personal_data_download_request/test_personal_data_download_request.py +++ b/frappe/website/doctype/personal_data_download_request/test_personal_data_download_request.py @@ -7,15 +7,19 @@ import frappe import unittest import json from frappe.website.doctype.personal_data_download_request.personal_data_download_request import get_user_data - +from frappe.contacts.doctype.contact.contact import get_contact_name class TestRequestPersonalData(unittest.TestCase): def setUp(self): create_user_if_not_exists(email='test_privacy@example.com') + def tearDown(self): + frappe.db.sql("""DELETE FROM `tabPersonal Data Download Request`""") + def test_user_data_creation(self): user_data = json.loads(get_user_data('test_privacy@example.com')) - expected_data = {'Contact': frappe.get_all('Contact', {'email_id':'test_privacy@example.com'}, ["*"])} + contact_name = get_contact_name('test_privacy@example.com') + expected_data = {'Contact': frappe.get_all('Contact', {"name": contact_name}, ["*"])} expected_data = json.loads(json.dumps(expected_data, default=str)) self.assertEqual({'Contact': user_data['Contact']}, expected_data) @@ -45,8 +49,7 @@ class TestRequestPersonalData(unittest.TestCase): frappe.db.sql("delete from `tabEmail Queue`") def create_user_if_not_exists(email, first_name = None): - if frappe.db.exists("User", email): - return + frappe.delete_doc_if_exists("User", email) frappe.get_doc({ "doctype": "User", diff --git a/frappe/www/login.html b/frappe/www/login.html index 8c470ac6dd..53e16d14c3 100644 --- a/frappe/www/login.html +++ b/frappe/www/login.html @@ -13,12 +13,12 @@
- +
@@ -66,12 +66,12 @@ @@ -84,9 +84,9 @@ + class="form-control" placeholder="{{ _('Email Address') }}" required autofocus>
diff --git a/frappe/www/login.py b/frappe/www/login.py index 6745ddea56..38beebf625 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -41,7 +41,7 @@ def get_context(context): ldap_settings = LDAPSettings.get_ldap_client_settings() context["ldap_settings"] = ldap_settings - login_name_placeholder = [_("Email address")] + login_name_placeholder = [_("Email Address")] if frappe.utils.cint(frappe.get_system_settings("allow_login_using_mobile_number")): login_name_placeholder.append(_("Mobile number"))