Merge pull request #35904 from GursheenK/voucher-wise-balance-report
feat: add voucher-wise balance report for unequal dr/cr GL entries
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index 924c14b..6fdb2f3 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -15,7 +15,6 @@
get_group_by_conditions,
get_tax_accounts,
)
-from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details
def execute(filters=None):
@@ -40,6 +39,16 @@
tax_doctype="Purchase Taxes and Charges",
)
+ scrubbed_tax_fields = {}
+
+ for tax in tax_columns:
+ scrubbed_tax_fields.update(
+ {
+ tax + " Rate": frappe.scrub(tax + " Rate"),
+ tax + " Amount": frappe.scrub(tax + " Amount"),
+ }
+ )
+
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
data = []
@@ -50,11 +59,7 @@
if filters.get("group_by"):
grand_total = get_grand_total(filters, "Purchase Invoice")
- item_details = get_item_details()
-
for d in item_list:
- item_record = item_details.get(d.item_code)
-
purchase_receipt = None
if d.purchase_receipt:
purchase_receipt = d.purchase_receipt
@@ -67,8 +72,8 @@
row = {
"item_code": d.item_code,
- "item_name": item_record.item_name if item_record else d.item_name,
- "item_group": item_record.item_group if item_record else d.item_group,
+ "item_name": d.pi_item_name if d.pi_item_name else d.i_item_name,
+ "item_group": d.pi_item_group if d.pi_item_group else d.i_item_group,
"description": d.description,
"invoice": d.parent,
"posting_date": d.posting_date,
@@ -101,8 +106,8 @@
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
{
- frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0),
- frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
+ scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
+ scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
}
)
total_tax += flt(item_tax.get("tax_amount"))
@@ -325,15 +330,17 @@
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
`tabPurchase Invoice`.unrealized_profit_loss_account,
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
- `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
+ `tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group,
+ `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
`tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
`tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {0}
- from `tabPurchase Invoice`, `tabPurchase Invoice Item`
+ from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
- `tabPurchase Invoice`.docstatus = 1 %s
+ `tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
+ `tabPurchase Invoice`.docstatus = 1 %s
""".format(
additional_query_columns
)
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index 0ebe13f..bd7d02e 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -11,7 +11,6 @@
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
get_customer_details,
- get_item_details,
)
@@ -35,6 +34,16 @@
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
+ scrubbed_tax_fields = {}
+
+ for tax in tax_columns:
+ scrubbed_tax_fields.update(
+ {
+ tax + " Rate": frappe.scrub(tax + " Rate"),
+ tax + " Amount": frappe.scrub(tax + " Amount"),
+ }
+ )
+
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
@@ -47,11 +56,9 @@
grand_total = get_grand_total(filters, "Sales Invoice")
customer_details = get_customer_details()
- item_details = get_item_details()
for d in item_list:
customer_record = customer_details.get(d.customer)
- item_record = item_details.get(d.item_code)
delivery_note = None
if d.delivery_note:
@@ -64,8 +71,8 @@
row = {
"item_code": d.item_code,
- "item_name": item_record.item_name if item_record else d.item_name,
- "item_group": item_record.item_group if item_record else d.item_group,
+ "item_name": d.si_item_name if d.si_item_name else d.i_item_name,
+ "item_group": d.si_item_group if d.si_item_group else d.i_item_group,
"description": d.description,
"invoice": d.parent,
"posting_date": d.posting_date,
@@ -107,8 +114,8 @@
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
{
- frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0),
- frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
+ scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
+ scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
}
)
if item_tax.get("is_other_charges"):
@@ -404,15 +411,18 @@
`tabSales Invoice Item`.project,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
+ `tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group,
+ `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
`tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
`tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0}
- from `tabSales Invoice`, `tabSales Invoice Item`
- where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
- and `tabSales Invoice`.docstatus = 1 {1}
+ from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem`
+ where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and
+ `tabItem`.name = `tabSales Invoice Item`.`item_code` and
+ `tabSales Invoice`.docstatus = 1 {1}
""".format(
additional_query_columns or "", conditions
),
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js
index 2df7db9..f9c6007 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.js
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.js
@@ -70,19 +70,21 @@
else if (frm.doc.purpose === 'Issue') {
fieldnames_to_be_altered = {
target_location: { read_only: 1, reqd: 0 },
- source_location: { read_only: 1, reqd: 1 },
+ source_location: { read_only: 1, reqd: 0 },
from_employee: { read_only: 1, reqd: 0 },
to_employee: { read_only: 0, reqd: 1 }
};
}
- Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
- let property_to_be_altered = fieldnames_to_be_altered[fieldname];
- Object.keys(property_to_be_altered).forEach(property => {
- let value = property_to_be_altered[property];
- frm.set_df_property(fieldname, property, value, cdn, 'assets');
+ if (fieldnames_to_be_altered) {
+ Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
+ let property_to_be_altered = fieldnames_to_be_altered[fieldname];
+ Object.keys(property_to_be_altered).forEach(property => {
+ let value = property_to_be_altered[property];
+ frm.fields_dict['assets'].grid.update_docfield_property(fieldname, property, value);
+ });
});
- });
- frm.refresh_field('assets');
+ frm.refresh_field('assets');
+ }
}
});
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json
index bdce639..5382f9e 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.json
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.json
@@ -37,6 +37,7 @@
"reqd": 1
},
{
+ "default": "Now",
"fieldname": "transaction_date",
"fieldtype": "Datetime",
"in_list_view": 1,
@@ -95,10 +96,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-01-22 12:30:55.295670",
+ "modified": "2023-06-28 16:54:26.571083",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Movement",
+ "naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
@@ -148,5 +150,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py
index 143f215..b58ca10 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -28,25 +28,20 @@
def validate_location(self):
for d in self.assets:
if self.purpose in ["Transfer", "Issue"]:
- if not d.source_location:
- d.source_location = frappe.db.get_value("Asset", d.asset, "location")
-
- if not d.source_location:
- frappe.throw(_("Source Location is required for the Asset {0}").format(d.asset))
-
+ current_location = frappe.db.get_value("Asset", d.asset, "location")
if d.source_location:
- current_location = frappe.db.get_value("Asset", d.asset, "location")
-
if current_location != d.source_location:
frappe.throw(
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
)
+ else:
+ d.source_location = current_location
if self.purpose == "Issue":
if d.target_location:
frappe.throw(
_(
- "Issuing cannot be done to a location. Please enter employee who has issued Asset {0}"
+ "Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
).format(d.asset),
title=_("Incorrect Movement Purpose"),
)
@@ -107,12 +102,12 @@
)
def on_submit(self):
- self.set_latest_location_in_asset()
+ self.set_latest_location_and_custodian_in_asset()
def on_cancel(self):
- self.set_latest_location_in_asset()
+ self.set_latest_location_and_custodian_in_asset()
- def set_latest_location_in_asset(self):
+ def set_latest_location_and_custodian_in_asset(self):
current_location, current_employee = "", ""
cond = "1=1"
diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py
index 72c0575..27e7e55 100644
--- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py
@@ -47,7 +47,7 @@
if not frappe.db.exists("Location", "Test Location 2"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
- movement1 = create_asset_movement(
+ create_asset_movement(
purpose="Transfer",
company=asset.company,
assets=[
@@ -58,7 +58,7 @@
)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
- create_asset_movement(
+ movement1 = create_asset_movement(
purpose="Transfer",
company=asset.company,
assets=[
@@ -70,21 +70,32 @@
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
movement1.cancel()
- self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
+ self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
employee = make_employee("testassetmovemp@example.com", company="_Test Company")
create_asset_movement(
purpose="Issue",
company=asset.company,
- assets=[{"asset": asset.name, "source_location": "Test Location", "to_employee": employee}],
+ assets=[{"asset": asset.name, "source_location": "Test Location 2", "to_employee": employee}],
reference_doctype="Purchase Receipt",
reference_name=pr.name,
)
- # after issuing asset should belong to an employee not at a location
+ # after issuing, asset should belong to an employee not at a location
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
+ create_asset_movement(
+ purpose="Receipt",
+ company=asset.company,
+ assets=[{"asset": asset.name, "from_employee": employee, "target_location": "Test Location"}],
+ reference_doctype="Purchase Receipt",
+ reference_name=pr.name,
+ )
+
+ # after receiving, asset should belong to a location not at an employee
+ self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
+
def test_last_movement_cancellation(self):
pr = make_purchase_receipt(
item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
diff --git a/erpnext/buying/doctype/supplier/patches/__init__.py b/erpnext/buying/doctype/supplier/patches/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/buying/doctype/supplier/patches/__init__.py
diff --git a/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py b/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py
new file mode 100644
index 0000000..5834952
--- /dev/null
+++ b/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py
@@ -0,0 +1,56 @@
+import os
+
+import frappe
+
+in_ci = os.environ.get("CI")
+
+
+def execute():
+ try:
+ contacts = get_portal_user_contacts()
+ add_portal_users(contacts)
+ except Exception:
+ frappe.db.rollback()
+ frappe.log_error("Failed to migrate portal users")
+
+ if in_ci: # TODO: better way to handle this.
+ raise
+
+
+def get_portal_user_contacts():
+ contact = frappe.qb.DocType("Contact")
+ dynamic_link = frappe.qb.DocType("Dynamic Link")
+
+ return (
+ frappe.qb.from_(contact)
+ .inner_join(dynamic_link)
+ .on(contact.name == dynamic_link.parent)
+ .select(
+ (dynamic_link.link_doctype).as_("doctype"),
+ (dynamic_link.link_name).as_("parent"),
+ (contact.email_id).as_("portal_user"),
+ )
+ .where(
+ (dynamic_link.parenttype == "Contact")
+ & (dynamic_link.link_doctype.isin(["Supplier", "Customer"]))
+ )
+ ).run(as_dict=True)
+
+
+def add_portal_users(contacts):
+ for contact in contacts:
+ user = frappe.db.get_value("User", {"email": contact.portal_user}, "name")
+ if not user:
+ continue
+
+ roles = frappe.get_roles(user)
+ required_role = contact.doctype
+ if required_role not in roles:
+ continue
+
+ portal_user_doc = frappe.new_doc("Portal User")
+ portal_user_doc.parenttype = contact.doctype
+ portal_user_doc.parentfield = "portal_users"
+ portal_user_doc.parent = contact.parent
+ portal_user_doc.user = user
+ portal_user_doc.insert()
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 1ae6f03..a536578 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -42,6 +42,14 @@
}
};
});
+
+ frm.set_query("user", "portal_users", function(doc) {
+ return {
+ filters: {
+ "ignore_user_type": true,
+ }
+ };
+ });
},
refresh: function (frm) {
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index f009789..b3b6185 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -68,7 +68,10 @@
"on_hold",
"hold_type",
"column_break_59",
- "release_date"
+ "release_date",
+ "portal_users_tab",
+ "portal_users",
+ "column_break_1mqv"
],
"fields": [
{
@@ -445,6 +448,21 @@
{
"fieldname": "column_break_59",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "portal_users_tab",
+ "fieldtype": "Tab Break",
+ "label": "Portal Users"
+ },
+ {
+ "fieldname": "portal_users",
+ "fieldtype": "Table",
+ "label": "Supplier Portal Users",
+ "options": "Portal User"
+ },
+ {
+ "fieldname": "column_break_1mqv",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-user",
@@ -457,7 +475,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-05-09 15:34:13.408932",
+ "modified": "2023-06-26 14:20:00.961554",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
@@ -489,7 +507,6 @@
"read": 1,
"report": 1,
"role": "Purchase Master Manager",
- "set_user_permissions": 1,
"share": 1,
"write": 1
},
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 01b5c8f..31bf439 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -16,6 +16,7 @@
get_timeline_data,
validate_party_accounts,
)
+from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
from erpnext.utilities.transaction_base import TransactionBase
@@ -46,12 +47,35 @@
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def on_update(self):
- if not self.naming_series:
- self.naming_series = ""
-
self.create_primary_contact()
self.create_primary_address()
+ def add_role_for_user(self):
+ for portal_user in self.portal_users:
+ add_role_for_portal_user(portal_user, "Supplier")
+
+ def _add_supplier_role(self, portal_user):
+ if not portal_user.is_new():
+ return
+
+ user_doc = frappe.get_doc("User", portal_user.user)
+ roles = {r.role for r in user_doc.roles}
+
+ if "Supplier" in roles:
+ return
+
+ if "System Manager" not in frappe.get_roles():
+ frappe.msgprint(
+ _("Please add 'Supplier' role to user {0}.").format(portal_user.user),
+ alert=True,
+ )
+ return
+
+ user_doc.add_roles("Supplier")
+ frappe.msgprint(
+ _("Added Supplier Role to User {0}.").format(frappe.bold(user_doc.name)), alert=True
+ )
+
def validate(self):
self.flags.is_new_doc = self.is_new()
@@ -62,6 +86,7 @@
validate_party_accounts(self)
self.validate_internal_supplier()
+ self.add_role_for_user()
@frappe.whitelist()
def get_supplier_group_details(self):
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 7a205ac..7be1d83 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -7,6 +7,7 @@
from frappe.test_runner import make_test_records
from erpnext.accounts.party import get_due_date
+from erpnext.controllers.website_list_for_contact import get_customers_suppliers
from erpnext.exceptions import PartyDisabled
test_dependencies = ["Payment Term", "Payment Terms Template"]
@@ -195,6 +196,9 @@
def create_supplier(**args):
args = frappe._dict(args)
+ if not args.supplier_name:
+ args.supplier_name = frappe.generate_hash()
+
if frappe.db.exists("Supplier", args.supplier_name):
return frappe.get_doc("Supplier", args.supplier_name)
@@ -209,3 +213,25 @@
).insert()
return doc
+
+
+class TestSupplierPortal(FrappeTestCase):
+ def test_portal_user_can_access_supplier_data(self):
+
+ supplier = create_supplier()
+
+ user = frappe.generate_hash() + "@example.com"
+ frappe.new_doc(
+ "User",
+ first_name="Supplier Portal User",
+ email=user,
+ send_welcome_email=False,
+ ).insert()
+
+ supplier.append("portal_users", {"user": user})
+ supplier.save()
+
+ frappe.set_user(user)
+ _, suppliers = get_customers_suppliers("Purchase Order", user)
+
+ self.assertIn(supplier.name, suppliers)
diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py
index 7c3c387..642722a 100644
--- a/erpnext/controllers/website_list_for_contact.py
+++ b/erpnext/controllers/website_list_for_contact.py
@@ -232,22 +232,8 @@
has_supplier_field = meta.has_field("supplier")
if has_common(["Supplier", "Customer"], frappe.get_roles(user)):
- contacts = frappe.db.sql(
- """
- select
- `tabContact`.email_id,
- `tabDynamic Link`.link_doctype,
- `tabDynamic Link`.link_name
- from
- `tabContact`, `tabDynamic Link`
- where
- `tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s
- """,
- user,
- as_dict=1,
- )
- customers = [c.link_name for c in contacts if c.link_doctype == "Customer"]
- suppliers = [c.link_name for c in contacts if c.link_doctype == "Supplier"]
+ suppliers = get_parents_for_user("Supplier")
+ customers = get_parents_for_user("Customer")
elif frappe.has_permission(doctype, "read", user=user):
customer_list = frappe.get_list("Customer")
customers = suppliers = [customer.name for customer in customer_list]
@@ -255,6 +241,17 @@
return customers if has_customer_field else None, suppliers if has_supplier_field else None
+def get_parents_for_user(parenttype: str) -> list[str]:
+ portal_user = frappe.qb.DocType("Portal User")
+
+ return (
+ frappe.qb.from_(portal_user)
+ .select(portal_user.parent)
+ .where(portal_user.user == frappe.session.user)
+ .where(portal_user.parenttype == parenttype)
+ ).run(pluck="name")
+
+
def has_website_permission(doc, ptype, user, verbose=False):
doctype = doc.doctype
customers, suppliers = get_customers_suppliers(doctype, user)
@@ -282,3 +279,28 @@
return "party_name"
else:
return "customer"
+
+
+def add_role_for_portal_user(portal_user, role):
+ """When a new portal user is added, give appropriate roles to user if
+ posssible, else warn user to add roles."""
+ if not portal_user.is_new():
+ return
+
+ user_doc = frappe.get_doc("User", portal_user.user)
+ roles = {r.role for r in user_doc.roles}
+
+ if role in roles:
+ return
+
+ if "System Manager" not in frappe.get_roles():
+ frappe.msgprint(
+ _("Please add {1} role to user {0}.").format(portal_user.user, role),
+ alert=True,
+ )
+ return
+
+ user_doc.add_roles(role)
+ frappe.msgprint(
+ _("Added {1} Role to User {0}.").format(frappe.bold(user_doc.name), role), alert=True
+ )
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py
index 55db29a..3654d29 100644
--- a/erpnext/crm/doctype/social_media_post/social_media_post.py
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.py
@@ -74,7 +74,7 @@
def process_scheduled_social_media_posts():
- posts = frappe.get_list(
+ posts = frappe.get_all(
"Social Media Post",
filters={"post_status": "Scheduled", "docstatus": 1},
fields=["name", "scheduled_time"],
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 8c0fa6b..fe6346e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -339,4 +339,5 @@
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True)
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True)
erpnext.patches.v14_0.cleanup_workspaces
-erpnext.patches.v14_0.set_report_in_process_SOA
+erpnext.patches.v14_0.set_report_in_process_SOA
+erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 2b3392a..f007430 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -364,7 +364,8 @@
"default": "0",
"fieldname": "collect_progress",
"fieldtype": "Check",
- "label": "Collect Progress"
+ "label": "Collect Progress",
+ "search_index": 1
},
{
"depends_on": "collect_progress",
@@ -451,7 +452,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2023-04-17 21:11:11.346986",
+ "modified": "2023-06-28 18:57:11.603497",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
diff --git a/erpnext/projects/doctype/project_update/project_update.json b/erpnext/projects/doctype/project_update/project_update.json
index 497b2b7..c548111 100644
--- a/erpnext/projects/doctype/project_update/project_update.json
+++ b/erpnext/projects/doctype/project_update/project_update.json
@@ -1,355 +1,106 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "naming_series:",
- "beta": 0,
- "creation": "2018-01-18 09:44:47.565494",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2018-01-18 09:44:47.565494",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "project",
+ "sent",
+ "column_break_2",
+ "date",
+ "time",
+ "section_break_5",
+ "users",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "naming_series",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Series",
- "length": 0,
- "no_copy": 0,
- "options": "PROJ-UPD-.YYYY.-",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "naming_series",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Series",
+ "options": "PROJ-UPD-.YYYY.-"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "project",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Project",
- "length": 0,
- "no_copy": 0,
- "options": "Project",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Project",
+ "options": "Project",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "fieldname": "sent",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Sent",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "sent",
+ "fieldtype": "Check",
+ "label": "Sent",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "read_only": 1,
+ "search_index": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "time",
+ "fieldtype": "Time",
+ "label": "Time",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "users",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Users",
- "length": 0,
- "no_copy": 0,
- "options": "Project User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "users",
+ "fieldtype": "Table",
+ "label": "Users",
+ "options": "Project User"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Project Update",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Project Update",
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-01-16 19:31:05.210656",
- "modified_by": "Administrator",
- "module": "Projects",
- "name": "Project Update",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-06-28 18:59:50.678917",
+ "modified_by": "Administrator",
+ "module": "Projects",
+ "name": "Project Update",
+ "naming_rule": "By \"Naming Series\" field",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Projects User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Projects User",
+ "share": 1,
+ "submit": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js
index b53f339..3a446e1 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -63,6 +63,14 @@
}
}
});
+
+ frm.set_query("user", "portal_users", function() {
+ return {
+ filters: {
+ "ignore_user_type": true,
+ }
+ };
+ });
},
customer_primary_address: function(frm){
if(frm.doc.customer_primary_address){
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 72a1594..edfe005 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -81,7 +81,9 @@
"dn_required",
"column_break_53",
"is_frozen",
- "disabled"
+ "disabled",
+ "portal_users_tab",
+ "portal_users"
],
"fields": [
{
@@ -555,6 +557,17 @@
{
"fieldname": "column_break_54",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "portal_users_tab",
+ "fieldtype": "Tab Break",
+ "label": "Portal Users"
+ },
+ {
+ "fieldname": "portal_users",
+ "fieldtype": "Table",
+ "label": "Customer Portal Users",
+ "options": "Portal User"
}
],
"icon": "fa fa-user",
@@ -568,7 +581,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-05-09 15:38:40.255193",
+ "modified": "2023-06-22 13:21:10.678382",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
@@ -607,7 +620,6 @@
"read": 1,
"report": 1,
"role": "Sales Master Manager",
- "set_user_permissions": 1,
"share": 1,
"write": 1
},
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 6367e3c..555db59 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -22,6 +22,7 @@
get_timeline_data,
validate_party_accounts,
)
+from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
from erpnext.utilities.transaction_base import TransactionBase
@@ -82,6 +83,7 @@
self.check_customer_group_change()
self.validate_default_bank_account()
self.validate_internal_customer()
+ self.add_role_for_user()
# set loyalty program tier
if frappe.db.exists("Customer", self.name):
@@ -170,6 +172,10 @@
self.update_customer_groups()
+ def add_role_for_user(self):
+ for portal_user in self.portal_users:
+ add_role_for_portal_user(portal_user, "Customer")
+
def update_customer_groups(self):
ignore_doctypes = ["Lead", "Opportunity", "POS Profile", "Tax Rule", "Pricing Rule"]
if frappe.flags.customer_group_changed:
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 6f1f981..31a3ecb 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -590,7 +590,7 @@
let selected_attributes = {};
me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
if(i===0) return;
- let attribute_name = $(col).find('.control-label').html().trim();
+ let attribute_name = $(col).find('.column-label').html().trim();
selected_attributes[attribute_name] = [];
let checked_opts = $(col).find('.checkbox input');
checked_opts.each((i, opt) => {
diff --git a/erpnext/tests/test_webform.py b/erpnext/tests/test_webform.py
index 202467b..af50a05 100644
--- a/erpnext/tests/test_webform.py
+++ b/erpnext/tests/test_webform.py
@@ -3,18 +3,21 @@
import frappe
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+from erpnext.buying.doctype.supplier.test_supplier import create_supplier
class TestWebsite(unittest.TestCase):
def test_permission_for_custom_doctype(self):
create_user("Supplier 1", "supplier1@gmail.com")
create_user("Supplier 2", "supplier2@gmail.com")
- create_supplier_with_contact(
- "Supplier1", "All Supplier Groups", "Supplier 1", "supplier1@gmail.com"
- )
- create_supplier_with_contact(
- "Supplier2", "All Supplier Groups", "Supplier 2", "supplier2@gmail.com"
- )
+
+ supplier1 = create_supplier(supplier_name="Supplier1")
+ supplier2 = create_supplier(supplier_name="Supplier2")
+ supplier1.append("portal_users", {"user": "supplier1@gmail.com"})
+ supplier1.save()
+ supplier2.append("portal_users", {"user": "supplier2@gmail.com"})
+ supplier2.save()
+
po1 = create_purchase_order(supplier="Supplier1")
po2 = create_purchase_order(supplier="Supplier2")
@@ -61,21 +64,6 @@
).insert(ignore_if_duplicate=True)
-def create_supplier_with_contact(name, group, contact_name, contact_email):
- supplier = frappe.get_doc(
- {"doctype": "Supplier", "supplier_name": name, "supplier_group": group}
- ).insert(ignore_if_duplicate=True)
-
- if not frappe.db.exists("Contact", contact_name + "-1-" + name):
- new_contact = frappe.new_doc("Contact")
- new_contact.first_name = contact_name
- new_contact.is_primary_contact = (True,)
- new_contact.append("links", {"link_doctype": "Supplier", "link_name": supplier.name})
- new_contact.append("email_ids", {"email_id": contact_email, "is_primary": 1})
-
- new_contact.insert(ignore_mandatory=True)
-
-
def create_custom_doctype():
frappe.get_doc(
{
diff --git a/erpnext/utilities/doctype/portal_user/__init__.py b/erpnext/utilities/doctype/portal_user/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/utilities/doctype/portal_user/__init__.py
diff --git a/erpnext/utilities/doctype/portal_user/portal_user.json b/erpnext/utilities/doctype/portal_user/portal_user.json
new file mode 100644
index 0000000..361166c
--- /dev/null
+++ b/erpnext/utilities/doctype/portal_user/portal_user.json
@@ -0,0 +1,34 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-06-20 14:01:35.362233",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User",
+ "reqd": 1,
+ "search_index": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-06-26 14:15:34.695605",
+ "modified_by": "Administrator",
+ "module": "Utilities",
+ "name": "Portal User",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/utilities/doctype/portal_user/portal_user.py b/erpnext/utilities/doctype/portal_user/portal_user.py
new file mode 100644
index 0000000..2e0064d
--- /dev/null
+++ b/erpnext/utilities/doctype/portal_user/portal_user.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class PortalUser(Document):
+ pass