Merge branch 'develop' into pr-item-gl-fix

This commit is contained in:
Saqib 2021-06-24 17:39:36 +05:30 committed by GitHub
commit c10fea8a59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 970 additions and 247 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.5.1' __version__ = '13.5.2'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
if address and frappe.db.get_value('Dynamic Link', if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}): {'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address]) filters.append(["Address", "name", "=", address])
if not address:
filters.append(["Address", "is_shipping_address", "=", 1])
address = frappe.get_all("Address", filters=filters, fields=fields) or {} address = frappe.get_all("Address", filters=filters, fields=fields) or {}

View File

@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
amount, base_amount = calculate_amount(doc, item, last_gl_entry, amount, base_amount = calculate_amount(doc, item, last_gl_entry,
total_days, total_booking_days, account_currency) total_days, total_booking_days, account_currency)
if not amount:
return
if via_journal_entry: if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)

View File

@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
doc: frm.doc, doc: frm.doc,
btn: $(btn_primary), btn: $(btn_primary),
method: "make_invoices", method: "make_invoices",
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]) freeze: 1,
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
callback: function(r) {
if (r.message.length == 1) {
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
} else if (r.message.length < 50) {
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
}
}
}); });
}); });

View File

@ -216,7 +216,8 @@ def start_import(invoices):
return names return names
def publish(index, total, doctype): def publish(index, total, doctype):
if total < 5: return if total < 50:
return
frappe.publish_realtime( frappe.publish_realtime(
"opening_invoice_creation_progress", "opening_invoice_creation_progress",
dict( dict(
@ -241,4 +242,3 @@ def get_temporary_opening_account(company=None):
return accounts[0].name return accounts[0].name

View File

@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}); });
} }
company() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
}
onload() { onload() {
super.onload(); super.onload();
@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
frm: frm, frm: frm,
freeze_message: __("Creating Purchase Receipt ...") freeze_message: __("Creating Purchase Receipt ...")
}) })
} },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
}) })

View File

@ -168,21 +168,24 @@ def get_columns(filters):
"label": _("Income"), "label": _("Income"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 305
}, },
{ {
"fieldname": "expense", "fieldname": "expense",
"label": _("Expense"), "label": _("Expense"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 305
}, },
{ {
"fieldname": "gross_profit_loss", "fieldname": "gross_profit_loss",
"label": _("Gross Profit / Loss"), "label": _("Gross Profit / Loss"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 307
} }
] ]

View File

@ -9,13 +9,14 @@
"supp_master_name", "supp_master_name",
"supplier_group", "supplier_group",
"buying_price_list", "buying_price_list",
"maintain_same_rate_action",
"role_to_override_stop_action",
"column_break_3", "column_break_3",
"po_required", "po_required",
"pr_required", "pr_required",
"maintain_same_rate", "maintain_same_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
@ -108,6 +109,13 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Role Allowed to Override Stop Action", "label": "Role Allowed to Override Stop Action",
"options": "Role" "options": "Role"
},
{
"default": "1",
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -115,7 +123,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-04-04 20:01:44.087066", "modified": "2021-06-23 19:40:00.120822",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -828,8 +828,14 @@ class AccountsController(TransactionBase):
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles(): if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") if self.doctype != "Purchase Invoice":
.format(item.item_code, item.idx, max_allowed_amt)) self.throw_overbill_exception(item, max_allowed_amt)
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
self.throw_overbill_exception(item, max_allowed_amt)
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname): def get_company_default(self, fieldname):
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default

View File

@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
fields = get_fields("Employee", ["name", "employee_name"]) fields = get_fields("Employee", ["name", "employee_name"])
return frappe.db.sql("""select {fields} from `tabEmployee` return frappe.db.sql("""select {fields} from `tabEmployee`
where status = 'Active' where status in ('Active', 'Suspended')
and docstatus < 2 and docstatus < 2
and ({key} like %(txt)s and ({key} like %(txt)s
or employee_name like %(txt)s) or employee_name like %(txt)s)

View File

@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
get_chart_data: function (_columns, result) { get_chart_data: function (_columns, result) {
return { return {
data: { data: {
labels: result.map(d => d[0]), labels: result.map(d => d.creation_date),
datasets: [{ datasets: [{
name: "First Response Time", name: "First Response Time",
values: result.map(d => d[1]) values: result.map(d => d.first_response_time)
}] }]
}, },
type: "line", type: "line",
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
hide_days: 0, hide_days: 0,
hide_seconds: 0 hide_seconds: 0
}; };
value = frappe.utils.get_formatted_duration(d, duration_options); return frappe.utils.get_formatted_duration(d, duration_options);
return value;
} }
} }
} }

View File

@ -207,7 +207,7 @@
"label": "Status", "label": "Status",
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Active\nInactive\nLeft", "options": "Active\nInactive\nSuspended\nLeft",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@ -813,7 +813,7 @@
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2021-06-12 11:31:37.730760", "modified": "2021-06-17 11:31:37.730760",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr from frappe.utils import getdate, validate_email_address, today, add_years, cstr
from frappe.model.naming import set_name_by_naming_series from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.permissions import add_user_permission, remove_user_permission, \
@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass
@ -37,7 +36,7 @@ class Employee(NestedSet):
def validate(self): def validate(self):
from erpnext.controllers.status_updater import validate_status from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Active", "Inactive", "Left"]) validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
self.employee = self.name self.employee = self.name
self.set_employee_name() self.set_employee_name()

View File

@ -7,7 +7,8 @@ def get_data():
'heatmap_message': _('This is based on the attendance of this Employee'), 'heatmap_message': _('This is based on the attendance of this Employee'),
'fieldname': 'employee', 'fieldname': 'employee',
'non_standard_fieldnames': { 'non_standard_fieldnames': {
'Bank Account': 'party' 'Bank Account': 'party',
'Employee Grievance': 'raised_by'
}, },
'transactions': [ 'transactions': [
{ {
@ -20,7 +21,7 @@ def get_data():
}, },
{ {
'label': _('Lifecycle'), 'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation'] 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
}, },
{ {
'label': _('Shift'), 'label': _('Shift'),

View File

@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
filters: [["status","=", "Active"]], filters: [["status","=", "Active"]],
get_indicator: function(doc) { get_indicator: function(doc) {
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status];
return indicator; return indicator;
} }
}; };

View File

@ -0,0 +1,39 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Employee Grievance', {
setup: function(frm) {
frm.set_query('grievance_against_party', function() {
return {
filters: {
name: ['in', [
'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee']
]
}
};
});
frm.set_query('associated_document_type', function() {
let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website",
"Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"];
return {
filters: {
istable: 0,
issingle: 0,
module: ["Not In", ignore_modules]
}
};
});
},
grievance_against_party: function(frm) {
let filters = {};
if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) {
filters.name = ["!=", frm.doc.raised_by];
}
frm.set_query('grievance_against', function() {
return {
filters: filters
};
});
},
});

View File

@ -0,0 +1,261 @@
{
"actions": [],
"autoname": "HR-GRIEV-.YYYY.-.#####",
"creation": "2021-05-11 13:41:51.485295",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"subject",
"raised_by",
"employee_name",
"designation",
"column_break_3",
"date",
"status",
"reports_to",
"grievance_details_section",
"grievance_against_party",
"grievance_against",
"grievance_type",
"column_break_11",
"associated_document_type",
"associated_document",
"section_break_14",
"description",
"investigation_details_section",
"cause_of_grievance",
"resolution_details_section",
"resolved_by",
"resolution_date",
"employee_responsible",
"column_break_16",
"resolution_detail",
"amended_from"
],
"fields": [
{
"fieldname": "grievance_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Grievance Type",
"options": "Grievance Type",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date ",
"reqd": 1
},
{
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Open\nInvestigated\nResolved\nInvalid",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"reqd": 1
},
{
"fieldname": "cause_of_grievance",
"fieldtype": "Text",
"label": "Cause of Grievance",
"mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\""
},
{
"fieldname": "resolution_details_section",
"fieldtype": "Section Break",
"label": "Resolution Details"
},
{
"fieldname": "resolved_by",
"fieldtype": "Link",
"label": "Resolved By",
"mandatory_depends_on": "eval: doc.status == \"Resolved\"",
"options": "User"
},
{
"fieldname": "employee_responsible",
"fieldtype": "Link",
"label": "Employee Responsible ",
"options": "Employee"
},
{
"fieldname": "resolution_detail",
"fieldtype": "Small Text",
"label": "Resolution Details",
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "resolution_date",
"fieldtype": "Date",
"label": "Resolution Date",
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
},
{
"fieldname": "grievance_against",
"fieldtype": "Dynamic Link",
"label": "Grievance Against",
"options": "grievance_against_party",
"reqd": 1
},
{
"fieldname": "raised_by",
"fieldtype": "Link",
"label": "Raised By",
"options": "Employee",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Grievance",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "raised_by.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fetch_from": "raised_by.reports_to",
"fieldname": "reports_to",
"fieldtype": "Link",
"label": "Reports To",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "grievance_details_section",
"fieldtype": "Section Break",
"label": "Grievance Details"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break"
},
{
"fieldname": "grievance_against_party",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Grievance Against Party",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "associated_document_type",
"fieldtype": "Link",
"label": "Associated Document Type",
"options": "DocType"
},
{
"fieldname": "associated_document",
"fieldtype": "Dynamic Link",
"label": "Associated Document",
"options": "associated_document_type"
},
{
"fieldname": "investigation_details_section",
"fieldtype": "Section Break",
"label": "Investigation Details"
},
{
"fetch_from": "raised_by.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-21 12:51:01.499486",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grievance",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"search_fields": "subject,raised_by,grievance_against_party",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
"track_changes": 1
}

View File

@ -0,0 +1,15 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, bold
from frappe.model.document import Document
class EmployeeGrievance(Document):
def on_submit(self):
if self.status not in ["Invalid", "Resolved"]:
frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
bold("Invalid"),
bold("Resolved"))
)

View File

@ -0,0 +1,12 @@
frappe.listview_settings["Employee Grievance"] = {
has_indicator_for_draft: 1,
get_indicator: function(doc) {
var colors = {
"Open": "red",
"Investigated": "orange",
"Resolved": "green",
"Invalid": "grey"
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View File

@ -0,0 +1,51 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
import unittest
from frappe.utils import today
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeGrievance(unittest.TestCase):
def test_create_employee_grievance(self):
create_employee_grievance()
def create_employee_grievance():
grievance_type = create_grievance_type()
emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
emp_2 = make_employee("testculprit@example.com", company="_Test Company")
grievance = frappe.new_doc("Employee Grievance")
grievance.subject = "Test Employee Grievance"
grievance.raised_by = emp_1
grievance.date = today()
grievance.grievance_type = grievance_type
grievance.grievance_against_party = "Employee"
grievance.grievance_against = emp_2
grievance.description = "test descrip"
#set cause
grievance.cause_of_grievance = "test cause"
#resolution details
grievance.resolution_date = today()
grievance.resolution_detail = "test resolution detail"
grievance.resolved_by = "test_emp_grievance_@example.com"
grievance.employee_responsible = emp_2
grievance.status = "Resolved"
grievance.save()
grievance.submit()
return grievance
def create_grievance_type():
if frappe.db.exists("Grievance Type", "Employee Abuse"):
return frappe.get_doc("Grievance Type", "Employee Abuse")
grievance_type = frappe.new_doc("Grievance Type")
grievance_type.name = "Employee Abuse"
grievance_type.description = "Test"
grievance_type.save()
return grievance_type.name

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Grievance Type', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,70 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2021-05-11 12:41:50.256071",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_5",
"description"
],
"fields": [
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-21 12:54:37.764712",
"modified_by": "Administrator",
"module": "HR",
"name": "Grievance Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class GrievanceType(Document):
pass

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestGrievanceType(unittest.TestCase):
pass

View File

@ -2,7 +2,7 @@
// MIT License. See license.txt // MIT License. See license.txt
frappe.listview_settings['Job Applicant'] = { frappe.listview_settings['Job Applicant'] = {
add_fields: ["company", "designation", "job_applicant", "status"], add_fields: ["status"],
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status == "Accepted") { if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status]; return [__(doc.status), "green", "status,=," + doc.status];

View File

@ -110,6 +110,7 @@
"label": "Allocation" "label": "Allocation"
}, },
{ {
"allow_on_submit": 1,
"bold": 1, "bold": 1,
"fieldname": "new_leaves_allocated", "fieldname": "new_leaves_allocated",
"fieldtype": "Float", "fieldtype": "Float",
@ -235,7 +236,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-14 15:28:26.335104", "modified": "2021-06-03 15:28:26.335104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Allocation", "name": "Leave Allocation",
@ -277,4 +278,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"timeline_field": "employee" "timeline_field": "employee"
} }

View File

@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.utils import set_employee_name, get_leave_period
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class BackDatedAllocationError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass
@ -55,6 +56,43 @@ class LeaveAllocation(Document):
if self.carry_forward: if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
def on_update_after_submit(self):
if self.has_value_changed("new_leaves_allocated"):
self.validate_against_leave_applications()
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
args = {
"leaves": leaves_to_be_added,
"from_date": self.from_date,
"to_date": self.to_date,
"is_carry_forward": 0
}
create_leave_ledger_entry(self, args, True)
def get_existing_leave_count(self):
ledger_entries = frappe.get_all("Leave Ledger Entry",
filters={
"transaction_type": "Leave Allocation",
"transaction_name": self.name,
"employee": self.employee,
"company": self.company,
"leave_type": self.leave_type
},
pluck="leaves")
total_existing_leaves = 0
for entry in ledger_entries:
total_existing_leaves += entry
return total_existing_leaves
def validate_against_leave_applications(self):
leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
self.from_date, self.to_date)
if flt(leaves_taken) > flt(self.total_leaves_allocated):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
else:
frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
def update_leave_policy_assignments_when_no_allocations_left(self): def update_leave_policy_assignments_when_no_allocations_left(self):
allocations = frappe.db.get_list("Leave Allocation", filters = { allocations = frappe.db.get_list("Leave Allocation", filters = {
"docstatus": 1, "docstatus": 1,
@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date):
def validate_carry_forward(leave_type): def validate_carry_forward(leave_type):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))

View File

@ -1,10 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import erpnext
import unittest import unittest
from frappe.utils import nowdate, add_months, getdate, add_days from frappe.utils import nowdate, add_months, getdate, add_days
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation
class TestLeaveAllocation(unittest.TestCase): class TestLeaveAllocation(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -164,6 +164,53 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.cancel() leave_allocation.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
def test_leave_addition_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 40
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
def test_leave_subtraction_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 10
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
def test_against_leave_application_validation_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
leave_application = frappe.get_doc({
"doctype": 'Leave Application',
"employee": employee.name,
"leave_type": "_Test Leave Type",
"from_date": nowdate(),
"to_date": add_days(nowdate(), 10),
"company": erpnext.get_default_company() or "_Test Company",
"docstatus": 1,
"status": "Approved",
"leave_approver": 'test@example.com'
})
leave_application.submit()
leave_allocation.new_leaves_allocated = 8
leave_allocation.total_leaves_allocated = 8
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
def create_leave_allocation(**args): def create_leave_allocation(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -103,4 +103,4 @@ var set_total_estimated_budget = function(frm) {
}) })
frm.set_value('total_estimated_budget', estimated_budget); frm.set_value('total_estimated_budget', estimated_budget);
} }
} };

View File

@ -41,7 +41,7 @@ class StaffingPlan(Document):
detail.total_estimated_cost = 0 detail.total_estimated_cost = 0
if detail.number_of_positions > 0: if detail.number_of_positions > 0:
if detail.vacancies > 0 and detail.estimated_cost_per_position: if detail.vacancies and detail.estimated_cost_per_position:
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
self.total_estimated_budget += detail.total_estimated_cost self.total_estimated_budget += detail.total_estimated_cost
@ -76,12 +76,12 @@ class StaffingPlan(Document):
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
for {2} as per staffing plan {3} for parent company {4}." for {2} as per staffing plan {3} for parent company {4}.").format(
.format(cint(parent_plan_details[0].vacancies), cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost, parent_plan_details[0].total_estimated_cost,
frappe.bold(staffing_plan_detail.designation), frappe.bold(staffing_plan_detail.designation),
parent_plan_details[0].name, parent_plan_details[0].name,
parent_company)), ParentCompanyError) parent_company), ParentCompanyError)
#Get vacanices already planned for all companies down the hierarchy of Parent Company #Get vacanices already planned for all companies down the hierarchy of Parent Company
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
@ -98,14 +98,14 @@ class StaffingPlan(Document):
(flt(parent_plan_details[0].total_estimated_cost) < \ (flt(parent_plan_details[0].total_estimated_cost) < \
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
.format(cint(all_sibling_details.vacancies), cint(all_sibling_details.vacancies),
all_sibling_details.total_estimated_cost, all_sibling_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation), frappe.bold(staffing_plan_detail.designation),
parent_company, parent_company,
cint(parent_plan_details[0].vacancies), cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost, parent_plan_details[0].total_estimated_cost,
parent_plan_details[0].name))) parent_plan_details[0].name))
def validate_with_subsidiary_plans(self, staffing_plan_detail): def validate_with_subsidiary_plans(self, staffing_plan_detail):
#Valdate this plan with all child company plan #Valdate this plan with all child company plan
@ -121,11 +121,11 @@ class StaffingPlan(Document):
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
.format(self.company, self.company,
cint(children_details.vacancies), cint(children_details.vacancies),
children_details.total_estimated_cost, children_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError) frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
@frappe.whitelist() @frappe.whitelist()
def get_designation_counts(designation, company): def get_designation_counts(designation, company):
@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
designation, from_date, to_date) designation, from_date, to_date)
# Only a single staffing plan can be active for a designation on given date # Only a single staffing plan can be active for a designation on given date
return staffing_plan if staffing_plan else None return staffing_plan if staffing_plan else None

View File

@ -11,8 +11,8 @@
"event": "Submit", "event": "Submit",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>{{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n {% endif %}\n <li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>Note: This Training Event is mandatory</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>", "message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>\n {{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b></li>\n {% endif %}\n <li>{{ _(\"Event Link\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>{{ _(\"Note: This Training Event is mandatory\") }}</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
"modified": "2021-05-24 16:29:13.165930", "modified": "2021-06-16 14:08:12.933367",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Training Scheduled", "name": "Training Scheduled",

View File

@ -24,19 +24,19 @@
{% set start = frappe.utils.get_datetime(doc.start_time) %} {% set start = frappe.utils.get_datetime(doc.start_time) %}
{% set end = frappe.utils.get_datetime(doc.end_time) %} {% set end = frappe.utils.get_datetime(doc.end_time) %}
{% if start.date() == end.date() %} {% if start.date() == end.date() %}
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li> <li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
<li> <li>
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b> {{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
</li> </li>
{% else %} {% else %}
<li>{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b> <li>
</li> {{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b> </li>
</li> <li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
{% endif %} {% endif %}
<li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li> <li>{{ _("Event Link") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
{% if doc.is_mandatory %} {% if doc.is_mandatory %}
<li>Note: This Training Event is mandatory</li> <li>{{ _("Note: This Training Event is mandatory") }}</li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@ -44,4 +44,4 @@
<td width="15"></td> <td width="15"></td>
</tr> </tr>
<tr height="10"></tr> <tr height="10"></tr>
</table> </table>

View File

@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
is_carry_forward, is_expired is_carry_forward, is_expired
FROM `tabLeave Ledger Entry` FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1 AND leaves>0 AND docstatus=1
AND (from_date between %(from_date)s AND %(to_date)s AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s)) OR (from_date < %(from_date)s AND to_date > %(to_date)s))

View File

@ -153,6 +153,24 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "Grievance Type",
"link_to": "Grievance Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grievance",
"link_to": "Employee Grievance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"dependencies": "Employee", "dependencies": "Employee",
"hidden": 0, "hidden": 0,
@ -823,7 +841,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-04-26 13:36:15.413819", "modified": "2021-05-13 17:19:40.524444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR", "name": "HR",

View File

@ -704,6 +704,8 @@ erpnext.work_order = {
stop_work_order: function(frm, status) { stop_work_order: function(frm, status) {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop", method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
freeze: true,
freeze_message: __("Updating Work Order status"),
args: { args: {
work_order: frm.doc.name, work_order: frm.doc.name,
status: status status: status

View File

@ -288,4 +288,5 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021 erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold

View File

@ -0,0 +1,8 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doctype("Buying Settings")
buying_settings = frappe.get_single("Buying Settings")
buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0
buying_settings.save()

View File

@ -12,8 +12,12 @@ frappe.ui.form.on('Additional Salary', {
} }
}; };
}); });
},
frm.trigger('set_earning_component'); onload: function(frm) {
if (frm.doc.type) {
frm.trigger('set_component_query');
}
}, },
employee: function(frm) { employee: function(frm) {
@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', {
}, },
company: function(frm) { company: function(frm) {
frm.trigger('set_earning_component'); frm.set_value("type", "");
frm.trigger('set_component_query');
}, },
set_earning_component: function(frm) { set_component_query: function(frm) {
if (!frm.doc.company) return; if (!frm.doc.company) return;
let filters = {company: frm.doc.company};
if (frm.doc.type) {
filters.type = frm.doc.type;
}
frm.set_query("salary_component", function() { frm.set_query("salary_component", function() {
return { return {
filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} filters: filters
}; };
}); });
}, },

View File

@ -11,6 +11,7 @@ from frappe import _
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from frappe.desk.reportview import get_match_cond, get_filters_cond from frappe.desk.reportview import get_match_cond, get_filters_cond
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
class PayrollEntry(Document): class PayrollEntry(Document):
def onload(self): def onload(self):
@ -41,7 +42,7 @@ class PayrollEntry(Document):
emp_with_sal_slip.append(employee_details.employee) emp_with_sal_slip.append(employee_details.employee)
if len(emp_with_sal_slip): if len(emp_with_sal_slip):
frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip))) frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
def on_cancel(self): def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
@ -211,7 +212,7 @@ class PayrollEntry(Document):
return account_dict return account_dict
def make_accrual_jv_entry(self): def make_accrual_jv_entry(self):
self.check_permission('write') self.check_permission("write")
earnings = self.get_salary_component_total(component_type = "earnings") or {} earnings = self.get_salary_component_total(component_type = "earnings") or {}
deductions = self.get_salary_component_total(component_type = "deductions") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {}
payroll_payable_account = self.payroll_payable_account payroll_payable_account = self.payroll_payable_account
@ -219,12 +220,13 @@ class PayrollEntry(Document):
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
if earnings or deductions: if earnings or deductions:
journal_entry = frappe.new_doc('Journal Entry') journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = 'Journal Entry' journal_entry.voucher_type = "Journal Entry"
journal_entry.user_remark = _('Accrual Journal Entry for salaries from {0} to {1}')\ journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\
.format(self.start_date, self.end_date) .format(self.start_date, self.end_date)
journal_entry.company = self.company journal_entry.company = self.company
journal_entry.posting_date = self.posting_date journal_entry.posting_date = self.posting_date
accounting_dimensions = get_accounting_dimensions() or []
accounts = [] accounts = []
currencies = [] currencies = []
@ -236,37 +238,34 @@ class PayrollEntry(Document):
for acc_cc, amount in earnings.items(): for acc_cc, amount in earnings.items():
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount += flt(amount, precision) payable_amount += flt(amount, precision)
accounts.append({ accounts.append(self.update_accounting_dimensions({
"account": acc_cc[0], "account": acc_cc[0],
"debit_in_account_currency": flt(amt, precision), "debit_in_account_currency": flt(amt, precision),
"exchange_rate": flt(exchange_rate), "exchange_rate": flt(exchange_rate),
"party_type": '',
"cost_center": acc_cc[1] or self.cost_center, "cost_center": acc_cc[1] or self.cost_center,
"project": self.project "project": self.project
}) }, accounting_dimensions))
# Deductions # Deductions
for acc_cc, amount in deductions.items(): for acc_cc, amount in deductions.items():
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount -= flt(amount, precision) payable_amount -= flt(amount, precision)
accounts.append({ accounts.append(self.update_accounting_dimensions({
"account": acc_cc[0], "account": acc_cc[0],
"credit_in_account_currency": flt(amt, precision), "credit_in_account_currency": flt(amt, precision),
"exchange_rate": flt(exchange_rate), "exchange_rate": flt(exchange_rate),
"cost_center": acc_cc[1] or self.cost_center, "cost_center": acc_cc[1] or self.cost_center,
"party_type": '',
"project": self.project "project": self.project
}) }, accounting_dimensions))
# Payable amount # Payable amount
exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies)
accounts.append({ accounts.append(self.update_accounting_dimensions({
"account": payroll_payable_account, "account": payroll_payable_account,
"credit_in_account_currency": flt(payable_amt, precision), "credit_in_account_currency": flt(payable_amt, precision),
"exchange_rate": flt(exchange_rate), "exchange_rate": flt(exchange_rate),
"party_type": '',
"cost_center": self.cost_center "cost_center": self.cost_center
}) }, accounting_dimensions))
journal_entry.set("accounts", accounts) journal_entry.set("accounts", accounts)
if len(currencies) > 1: if len(currencies) > 1:
@ -286,6 +285,12 @@ class PayrollEntry(Document):
return jv_name return jv_name
def update_accounting_dimensions(self, row, accounting_dimensions):
for dimension in accounting_dimensions:
row.update({dimension: self.get(dimension)})
return row
def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies):
conversion_rate = 1 conversion_rate = 1
exchange_rate = self.exchange_rate exchange_rate = self.exchange_rate

View File

@ -481,6 +481,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
if not salary_structure: if not salary_structure:
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
employee = frappe.db.get_value("Employee", {"user_id": user}) employee = frappe.db.get_value("Employee", {"user_id": user})
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})

View File

@ -124,8 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
"doctype": "Salary Structure", "doctype": "Salary Structure",
"name": salary_structure, "name": salary_structure,
"company": company or erpnext.get_default_company(), "company": company or erpnext.get_default_company(),
"earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency, "payroll_frequency": payroll_frequency,
"payment_account": get_random("Account", filters={'account_currency': currency}), "payment_account": get_random("Account", filters={'account_currency': currency}),
"currency": currency "currency": currency

View File

@ -41,6 +41,30 @@ class TestProductConfigurator(unittest.TestCase):
"show_variant_in_website": 1 "show_variant_in_website": 1
}).insert() }).insert()
def create_regular_web_item(self, name, item_group=None):
if not frappe.db.exists('Item', name):
doc = frappe.get_doc({
"description": name,
"item_code": name,
"item_name": name,
"doctype": "Item",
"is_stock_item": 1,
"item_group": item_group or "_Test Item Group",
"stock_uom": "_Test UOM",
"item_defaults": [{
"company": "_Test Company",
"default_warehouse": "_Test Warehouse - _TC",
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"buying_cost_center": "_Test Cost Center - _TC",
"selling_cost_center": "_Test Cost Center - _TC",
"income_account": "Sales - _TC"
}],
"show_in_website": 1
}).insert()
else:
doc = frappe.get_doc("Item", name)
return doc
def test_product_list(self): def test_product_list(self):
template_items = frappe.get_all('Item', {'show_in_website': 1}) template_items = frappe.get_all('Item', {'show_in_website': 1})
variant_items = frappe.get_all('Item', {'show_variant_in_website': 1}) variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
@ -77,3 +101,42 @@ class TestProductConfigurator(unittest.TestCase):
'Test Size': ['2XL'] 'Test Size': ['2XL']
}) })
self.assertEqual(len(items), 1) self.assertEqual(len(items), 1)
def test_products_in_multiple_item_groups(self):
"""Check if product is visible on multiple item group pages barring its own."""
from erpnext.shopping_cart.product_query import ProductQuery
if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
item_group_doc = frappe.get_doc({
"doctype": "Item Group",
"item_group_name": "Tech Items",
"parent_item_group": "All Item Groups",
"show_in_website": 1
}).insert()
else:
item_group_doc = frappe.get_doc("Item Group", "Tech Items")
doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
doc.append("website_item_groups", {
"item_group": "_Test Item Group Desktops"
})
doc.save()
# check if item is visible in its own Item Group's page
engine = ProductQuery()
items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
self.assertEqual(len(items), 1)
self.assertEqual(items[0].item_code, "Portal Item")
# check if item is visible in configured foreign Item Group's page
engine = ProductQuery()
items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
item_codes = [row.item_code for row in items]
self.assertIn(len(items), [2, 3])
self.assertIn("Portal Item", item_codes)
# teardown
doc.delete()
item_group_doc.delete()

View File

@ -888,9 +888,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
if (this.frm.doc.posting_date) var date = this.frm.doc.posting_date;
else var date = this.frm.doc.transaction_date;
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function(){ erpnext.utils.get_shipping_address(this.frm, function(){

View File

@ -274,9 +274,9 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
return true; return true;
} }
erpnext.utils.get_shipping_address = function(frm, callback){ erpnext.utils.get_shipping_address = function(frm, callback) {
if (frm.doc.company) { if (frm.doc.company) {
if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || if ((frm.doc.inter_company_order_reference || frm.doc.internal_invoice_reference ||
frm.doc.internal_order_reference)) { frm.doc.internal_order_reference)) {
if (callback) { if (callback) {
return callback(); return callback();

View File

@ -467,11 +467,15 @@ body.product-page {
.btn-change-address { .btn-change-address {
color: var(--blue-500); color: var(--blue-500);
box-shadow: none;
border: 1px solid var(--blue-500);
} }
} }
.btn-new-address:hover, .btn-change-address:hover {
box-shadow: none;
color: var(--blue-500) !important;
border: 1px solid var(--blue-500);
}
.modal .address-card { .modal .address-card {
.card-body { .card-body {
padding: var(--padding-sm); padding: var(--padding-sm);

View File

@ -241,8 +241,8 @@ erpnext.PointOfSale.Controller = class {
events: { events: {
get_frm: () => this.frm, get_frm: () => this.frm,
cart_item_clicked: (item_code, batch_no, uom, rate) => { cart_item_clicked: (item) => {
const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); const item_row = this.get_item_from_frm(item);
this.item_details.toggle_item_details_section(item_row); this.item_details.toggle_item_details_section(item_row);
}, },
@ -273,17 +273,15 @@ erpnext.PointOfSale.Controller = class {
this.cart.toggle_numpad(minimize); this.cart.toggle_numpad(minimize);
}, },
form_updated: (cdt, cdn, fieldname, value) => { form_updated: (item, field, value) => {
const item_row = frappe.model.get_doc(cdt, cdn); const item_row = frappe.model.get_doc(item.doctype, item.name);
if (item_row && item_row[fieldname] != value) { if (item_row && item_row[field] != value) {
const args = {
const { item_code, batch_no, uom, rate } = this.item_details.current_item; field,
const event = {
field: fieldname,
value, value,
item: { item_code, batch_no, uom, rate } item: this.item_details.current_item
} };
return this.on_cart_update(event) return this.on_cart_update(args);
} }
return Promise.resolve(); return Promise.resolve();
@ -300,19 +298,18 @@ erpnext.PointOfSale.Controller = class {
set_value_in_current_cart_item: (selector, value) => { set_value_in_current_cart_item: (selector, value) => {
this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item); this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item);
}, },
clone_new_batch_item_in_frm: (batch_serial_map, current_item) => { clone_new_batch_item_in_frm: (batch_serial_map, item) => {
// called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches
// for each unique batch new item row is added in the form & cart // for each unique batch new item row is added in the form & cart
Object.keys(batch_serial_map).forEach(batch => { Object.keys(batch_serial_map).forEach(batch => {
const { item_code, batch_no } = current_item; const item_to_clone = this.frm.doc.items.find(i => i.name == item.name);
const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no);
const new_row = this.frm.add_child("items", { ...item_to_clone }); const new_row = this.frm.add_child("items", { ...item_to_clone });
// update new serialno and batch // update new serialno and batch
new_row.batch_no = batch; new_row.batch_no = batch;
new_row.serial_no = batch_serial_map[batch].join(`\n`); new_row.serial_no = batch_serial_map[batch].join(`\n`);
new_row.qty = batch_serial_map[batch].length; new_row.qty = batch_serial_map[batch].length;
this.frm.doc.items.forEach(row => { this.frm.doc.items.forEach(row => {
if (item_code === row.item_code) { if (item.item_code === row.item_code) {
this.update_cart_html(row); this.update_cart_html(row);
} }
}); });
@ -321,8 +318,8 @@ erpnext.PointOfSale.Controller = class {
remove_item_from_cart: () => this.remove_item_from_cart(), remove_item_from_cart: () => this.remove_item_from_cart(),
get_item_stock_map: () => this.item_stock_map, get_item_stock_map: () => this.item_stock_map,
close_item_details: () => { close_item_details: () => {
this.item_details.toggle_item_details_section(undefined); this.item_details.toggle_item_details_section(null);
this.cart.prev_action = undefined; this.cart.prev_action = null;
this.cart.toggle_item_highlight(); this.cart.toggle_item_highlight();
}, },
get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse) get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse)
@ -506,50 +503,47 @@ erpnext.PointOfSale.Controller = class {
let item_row = undefined; let item_row = undefined;
try { try {
let { field, value, item } = args; let { field, value, item } = args;
const { item_code, batch_no, serial_no, uom, rate } = item; item_row = this.get_item_from_frm(item);
item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); const item_row_exists = !$.isEmptyObject(item_row);
const item_selected_from_selector = field === 'qty' && value === "+1" const from_selector = field === 'qty' && value === "+1";
if (from_selector)
value = flt(item_row.qty) + flt(value);
if (item_row) { if (item_row_exists) {
item_selected_from_selector && (value = item_row.qty + flt(value)) if (field === 'qty')
value = flt(value);
field === 'qty' && (value = flt(value));
if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) { if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) {
const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value; const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value;
await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
} }
if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) { if (this.is_current_item_being_edited(item_row) || from_selector) {
await frappe.model.set_value(item_row.doctype, item_row.name, field, value); await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
this.update_cart_html(item_row); this.update_cart_html(item_row);
} }
} else { } else {
if (!this.frm.doc.customer) { if (!this.frm.doc.customer)
frappe.dom.unfreeze(); return this.raise_customer_selection_alert();
frappe.show_alert({
message: __('You must select a customer before adding an item.'), const { item_code, batch_no, serial_no, rate } = item;
indicator: 'orange'
}); if (!item_code)
frappe.utils.play_sound("error");
return; return;
}
if (!item_code) return;
item_selected_from_selector && (value = flt(value)) const new_item = { item_code, batch_no, rate, [field]: value };
const args = { item_code, batch_no, rate, [field]: value };
if (serial_no) { if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
args['serial_no'] = serial_no; new_item['serial_no'] = serial_no;
} }
if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0; if (field === 'serial_no')
new_item['qty'] = value.split(`\n`).length || 0;
item_row = this.frm.add_child('items', args); item_row = this.frm.add_child('items', new_item);
if (field === 'qty' && value !== 0 && !this.allow_negative_stock) if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
@ -558,8 +552,11 @@ erpnext.PointOfSale.Controller = class {
this.update_cart_html(item_row); this.update_cart_html(item_row);
this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row); if (this.item_details.$component.is(':visible'))
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row); this.edit_item_details_of(item_row);
if (this.check_serial_batch_selection_needed(item_row))
this.edit_item_details_of(item_row);
} }
} catch (error) { } catch (error) {
@ -570,14 +567,33 @@ erpnext.PointOfSale.Controller = class {
} }
} }
get_item_from_frm(item_code, batch_no, uom, rate) { raise_customer_selection_alert() {
const has_batch_no = batch_no; frappe.dom.unfreeze();
return this.frm.doc.items.find( frappe.show_alert({
i => i.item_code === item_code message: __('You must select a customer before adding an item.'),
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) indicator: 'orange'
&& (i.uom === uom) });
&& (i.rate == rate) frappe.utils.play_sound("error");
); }
get_item_from_frm({ name, item_code, batch_no, uom, rate }) {
let item_row = null;
if (name) {
item_row = this.frm.doc.items.find(i => i.name == name);
} else {
// if item is clicked twice from item selector
// then "item_code, batch_no, uom, rate" will help in getting the exact item
// to increase the qty by one
const has_batch_no = batch_no;
item_row = this.frm.doc.items.find(
i => i.item_code === item_code
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
&& (i.uom === uom)
&& (i.rate == rate)
);
}
return item_row || {};
} }
edit_item_details_of(item_row) { edit_item_details_of(item_row) {
@ -585,9 +601,7 @@ erpnext.PointOfSale.Controller = class {
} }
is_current_item_being_edited(item_row) { is_current_item_being_edited(item_row) {
const { item_code, batch_no } = this.item_details.current_item; return item_row.name == this.item_details.current_item.name;
return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true;
} }
update_cart_html(item_row, remove_item) { update_cart_html(item_row, remove_item) {
@ -669,7 +683,7 @@ erpnext.PointOfSale.Controller = class {
update_item_field(value, field_or_action) { update_item_field(value, field_or_action) {
if (field_or_action === 'checkout') { if (field_or_action === 'checkout') {
this.item_details.toggle_item_details_section(undefined); this.item_details.toggle_item_details_section(null);
} else if (field_or_action === 'remove') { } else if (field_or_action === 'remove') {
this.remove_item_from_cart(); this.remove_item_from_cart();
} else { } else {
@ -688,7 +702,7 @@ erpnext.PointOfSale.Controller = class {
.then(() => { .then(() => {
frappe.model.clear_doc(doctype, name); frappe.model.clear_doc(doctype, name);
this.update_cart_html(current_item, true); this.update_cart_html(current_item, true);
this.item_details.toggle_item_details_section(undefined); this.item_details.toggle_item_details_section(null);
frappe.dom.unfreeze(); frappe.dom.unfreeze();
}) })
.catch(e => console.log(e)); .catch(e => console.log(e));

View File

@ -181,11 +181,8 @@ erpnext.PointOfSale.ItemCart = class {
me.$totals_section.find(".edit-cart-btn").click(); me.$totals_section.find(".edit-cart-btn").click();
} }
const item_code = unescape($cart_item.attr('data-item-code')); const item_row_name = unescape($cart_item.attr('data-row-name'));
const batch_no = unescape($cart_item.attr('data-batch-no')); me.events.cart_item_clicked({ name: item_row_name });
const uom = unescape($cart_item.attr('data-uom'));
const rate = unescape($cart_item.attr('data-rate'));
me.events.cart_item_clicked(item_code, batch_no, uom, rate);
this.numpad_value = ''; this.numpad_value = '';
}); });
@ -521,25 +518,14 @@ erpnext.PointOfSale.ItemCart = class {
} }
} }
get_cart_item({ item_code, batch_no, uom, rate }) { get_cart_item({ name }) {
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`;
const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
const uom_attr = `[data-uom="${escape(uom)}"]`;
const rate_attr = `[data-rate="${escape(rate)}"]`;
const item_selector = batch_no ?
`.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`;
return this.$cart_items_wrapper.find(item_selector); return this.$cart_items_wrapper.find(item_selector);
} }
get_item_from_frm(item) { get_item_from_frm(item) {
const doc = this.events.get_frm().doc; const doc = this.events.get_frm().doc;
const { item_code, batch_no, uom, rate } = item; return doc.items.find(i => i.name == item.name);
const search_field = batch_no ? 'batch_no' : 'item_code';
const search_value = batch_no || item_code;
return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate);
} }
update_item_html(item, remove_item) { update_item_html(item, remove_item) {
@ -564,10 +550,7 @@ erpnext.PointOfSale.ItemCart = class {
if (!$item_to_update.length) { if (!$item_to_update.length) {
this.$cart_items_wrapper.append( this.$cart_items_wrapper.append(
`<div class="cart-item-wrapper" `<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
data-batch-no="${escape(item_data.batch_no || '')}" data-rate="${escape(item_data.rate)}">
</div>
<div class="seperator"></div>` <div class="seperator"></div>`
) )
$item_to_update = this.get_cart_item(item_data); $item_to_update = this.get_cart_item(item_data);
@ -642,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class {
function get_item_image_html() { function get_item_image_html() {
const { image, item_name } = item_data; const { image, item_name } = item_data;
if (image) { if (!me.hide_images && image) {
return ` return `
<div class="item-image"> <div class="item-image">
<img <img

View File

@ -2,6 +2,7 @@ erpnext.PointOfSale.ItemDetails = class {
constructor({ wrapper, events, settings }) { constructor({ wrapper, events, settings }) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.events = events; this.events = events;
this.hide_images = settings.hide_images;
this.allow_rate_change = settings.allow_rate_change; this.allow_rate_change = settings.allow_rate_change;
this.allow_discount_change = settings.allow_discount_change; this.allow_discount_change = settings.allow_discount_change;
this.current_item = {}; this.current_item = {};
@ -54,36 +55,28 @@ erpnext.PointOfSale.ItemDetails = class {
this.$dicount_section = this.$component.find('.discount-section'); this.$dicount_section = this.$component.find('.discount-section');
} }
has_item_has_changed(item) { compare_with_current_item(item) {
const { item_code, batch_no, uom, rate } = this.current_item; // returns true if `item` is currently being edited
const item_code_is_same = item && item_code === item.item_code; return item && item.name == this.current_item.name;
const batch_is_same = item && batch_no == item.batch_no;
const uom_is_same = item && uom === item.uom;
const rate_is_same = item && rate === item.rate;
if (!item)
return false;
if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same)
return false;
return true;
} }
toggle_item_details_section(item) { toggle_item_details_section(item) {
this.item_has_changed = this.has_item_has_changed(item); const current_item_changed = !this.compare_with_current_item(item);
this.events.toggle_item_selector(this.item_has_changed); // if item is null or highlighted cart item is clicked twice
this.toggle_component(this.item_has_changed); const hide_item_details = !Boolean(item) || !current_item_changed;
this.events.toggle_item_selector(!hide_item_details);
this.toggle_component(!hide_item_details);
if (this.item_has_changed) { if (item && current_item_changed) {
this.doctype = item.doctype; this.doctype = item.doctype;
this.item_meta = frappe.get_meta(this.doctype); this.item_meta = frappe.get_meta(this.doctype);
this.name = item.name; this.name = item.name;
this.item_row = item; this.item_row = item;
this.currency = this.events.get_frm().doc.currency; this.currency = this.events.get_frm().doc.currency;
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate }; this.current_item = item;
this.render_dom(item); this.render_dom(item);
this.render_discount_dom(item); this.render_discount_dom(item);
@ -132,7 +125,7 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_name.html(item_name); this.$item_name.html(item_name);
this.$item_description.html(get_description_html()); this.$item_description.html(get_description_html());
this.$item_price.html(format_currency(price_list_rate, this.currency)); this.$item_price.html(format_currency(price_list_rate, this.currency));
if (image) { if (!this.hide_images && image) {
this.$item_image.html( this.$item_image.html(
`<img `<img
onerror="cur_pos.item_details.handle_broken_image(this)" onerror="cur_pos.item_details.handle_broken_image(this)"
@ -180,7 +173,7 @@ erpnext.PointOfSale.ItemDetails = class {
df: { df: {
...field_meta, ...field_meta,
onchange: function() { onchange: function() {
me.events.form_updated(me.doctype, me.name, fieldname, this.value); me.events.form_updated(me.current_item, fieldname, this.value);
} }
}, },
parent: this.$form_container.find(`.${fieldname}-control`), parent: this.$form_container.find(`.${fieldname}-control`),
@ -218,22 +211,17 @@ erpnext.PointOfSale.ItemDetails = class {
bind_custom_control_change_event() { bind_custom_control_change_event() {
const me = this; const me = this;
if (this.rate_control) { if (this.rate_control) {
if (this.allow_rate_change) { this.rate_control.df.onchange = function() {
this.rate_control.df.onchange = function() { if (this.value || flt(this.value) === 0) {
if (this.value || flt(this.value) === 0) { me.events.form_updated(me.current_item, 'rate', this.value).then(() => {
me.events.set_value_in_current_cart_item('rate', this.value); const item_row = frappe.get_doc(me.doctype, me.name);
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => { const doc = me.events.get_frm().doc;
const item_row = frappe.get_doc(me.doctype, me.name); me.$item_price.html(format_currency(item_row.rate, doc.currency));
const doc = me.events.get_frm().doc; me.render_discount_dom(item_row);
me.$item_price.html(format_currency(item_row.rate, doc.currency)); });
me.render_discount_dom(item_row); }
}); };
me.current_item.rate = this.value; this.rate_control.df.read_only = !this.allow_rate_change;
}
};
} else {
this.rate_control.df.read_only = 1;
}
this.rate_control.refresh(); this.rate_control.refresh();
} }
@ -246,7 +234,7 @@ erpnext.PointOfSale.ItemDetails = class {
this.warehouse_control.df.reqd = 1; this.warehouse_control.df.reqd = 1;
this.warehouse_control.df.onchange = function() { this.warehouse_control.df.onchange = function() {
if (this.value) { if (this.value) {
me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => { me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
me.item_stock_map = me.events.get_item_stock_map(); me.item_stock_map = me.events.get_item_stock_map();
const available_qty = me.item_stock_map[me.item_row.item_code][this.value]; const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
if (available_qty === undefined) { if (available_qty === undefined) {
@ -278,7 +266,7 @@ erpnext.PointOfSale.ItemDetails = class {
this.serial_no_control.df.reqd = 1; this.serial_no_control.df.reqd = 1;
this.serial_no_control.df.onchange = async function() { this.serial_no_control.df.onchange = async function() {
!me.current_item.batch_no && await me.auto_update_batch_no(); !me.current_item.batch_no && await me.auto_update_batch_no();
me.events.form_updated(me.doctype, me.name, 'serial_no', this.value); me.events.form_updated(me.current_item, 'serial_no', this.value);
} }
this.serial_no_control.refresh(); this.serial_no_control.refresh();
} }
@ -295,19 +283,12 @@ erpnext.PointOfSale.ItemDetails = class {
} }
} }
}; };
this.batch_no_control.df.onchange = function() {
me.events.set_value_in_current_cart_item('batch-no', this.value);
me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
me.current_item.batch_no = this.value;
}
this.batch_no_control.refresh(); this.batch_no_control.refresh();
} }
if (this.uom_control) { if (this.uom_control) {
this.uom_control.df.onchange = function() { this.uom_control.df.onchange = function() {
me.events.set_value_in_current_cart_item('uom', this.value); me.events.form_updated(me.current_item, 'uom', this.value);
me.events.form_updated(me.doctype, me.name, 'uom', this.value);
me.current_item.uom = this.value;
const item_row = frappe.get_doc(me.doctype, me.name); const item_row = frappe.get_doc(me.doctype, me.name);
me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value); me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value);
@ -317,9 +298,9 @@ erpnext.PointOfSale.ItemDetails = class {
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
const field_control = this[`${fieldname}_control`]; const field_control = this[`${fieldname}_control`];
const item_is_same = !this.has_item_has_changed(item_row); const item_row_is_being_edited = this.compare_with_current_item(item_row);
if (item_is_same && field_control && field_control.get_value() !== value) { if (item_row_is_being_edited && field_control && field_control.get_value() !== value) {
field_control.set_value(value); field_control.set_value(value);
cur_pos.update_cart_html(item_row); cur_pos.update_cart_html(item_row);
} }
@ -337,7 +318,9 @@ erpnext.PointOfSale.ItemDetails = class {
fields: ["batch_no", "name"] fields: ["batch_no", "name"]
}); });
const batch_serial_map = serials_with_batch_no.reduce((acc, r) => { const batch_serial_map = serials_with_batch_no.reduce((acc, r) => {
acc[r.batch_no] || (acc[r.batch_no] = []); if (!acc[r.batch_no]) {
acc[r.batch_no] = [];
}
acc[r.batch_no] = [...acc[r.batch_no], r.name]; acc[r.batch_no] = [...acc[r.batch_no], r.name];
return acc; return acc;
}, {}); }, {});
@ -353,12 +336,10 @@ erpnext.PointOfSale.ItemDetails = class {
if (serial_nos_belongs_to_other_batch) { if (serial_nos_belongs_to_other_batch) {
this.serial_no_control.set_value(batch_serial_nos); this.serial_no_control.set_value(batch_serial_nos);
this.qty_control.set_value(batch_serial_map[batch_no].length); this.qty_control.set_value(batch_serial_map[batch_no].length);
}
delete batch_serial_map[batch_no]; delete batch_serial_map[batch_no];
if (serial_nos_belongs_to_other_batch)
this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item); this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
}
} }
} }

View File

@ -232,7 +232,11 @@ erpnext.PointOfSale.ItemSelector = class {
uom = uom === "undefined" ? undefined : uom; uom = uom === "undefined" ? undefined : uom;
rate = rate === "undefined" ? undefined : rate; rate = rate === "undefined" ? undefined : rate;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }}); me.events.item_selected({
field: 'qty',
value: "+1",
item: { item_code, batch_no, serial_no, uom, rate }
});
me.set_search_value(''); me.set_search_value('');
}); });

View File

@ -59,7 +59,7 @@ def get_data(conditions, filters):
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
soi.qty, soi.delivered_qty, soi.qty, soi.delivered_qty,
(soi.qty - soi.delivered_qty) AS pending_qty, (soi.qty - soi.delivered_qty) AS pending_qty,
IFNULL(sii.qty, 0) as billed_qty, IFNULL(SUM(sii.qty), 0) as billed_qty,
soi.base_amount as amount, soi.base_amount as amount,
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount, (soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,

View File

@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
field_filters['item_group'] = self.name field_filters['item_group'] = self.name
engine = ProductQuery() engine = ProductQuery()
context.items = engine.query(attribute_filters, field_filters, search, start) context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
filter_engine = ProductFiltersBuilder(self.name) filter_engine = ProductFiltersBuilder(self.name)

View File

@ -22,12 +22,15 @@ class ProductFiltersBuilder:
filter_data = [] filter_data = []
for df in fields: for df in fields:
filters = {} filters, or_filters = {}, []
if df.fieldtype == "Link": if df.fieldtype == "Link":
if self.item_group: if self.item_group:
filters['item_group'] = self.item_group or_filters.extend([
["item_group", "=", self.item_group],
["Website Item Group", "item_group", "=", self.item_group]
])
values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname) values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname)
else: else:
doctype = df.get_link_doctype() doctype = df.get_link_doctype()
@ -44,7 +47,9 @@ class ProductFiltersBuilder:
values = [d.name for d in frappe.get_all(doctype, filters)] values = [d.name for d in frappe.get_all(doctype, filters)]
# Remove None # Remove None
values = values.remove(None) if None in values else values if None in values:
values.remove(None)
if values: if values:
filter_data.append([df, values]) filter_data.append([df, values])
@ -61,14 +66,18 @@ class ProductFiltersBuilder:
for attr_doc in attribute_docs: for attr_doc in attribute_docs:
selected_attributes = [] selected_attributes = []
for attr in attr_doc.item_attribute_values: for attr in attr_doc.item_attribute_values:
or_filters = []
filters= [ filters= [
["Item Variant Attribute", "attribute", "=", attr.parent], ["Item Variant Attribute", "attribute", "=", attr.parent],
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
] ]
if self.item_group: if self.item_group:
filters.append(["item_group", "=", self.item_group]) or_filters.extend([
["item_group", "=", self.item_group],
["Website Item Group", "item_group", "=", self.item_group]
])
if frappe.db.get_all("Item", filters, limit=1): if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
selected_attributes.append(attr) selected_attributes.append(attr)
if selected_attributes: if selected_attributes:

View File

@ -22,13 +22,14 @@ class ProductQuery:
self.settings = frappe.get_doc("Products Settings") self.settings = frappe.get_doc("Products Settings")
self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.cart_settings = frappe.get_doc("Shopping Cart Settings")
self.page_length = self.settings.products_per_page or 20 self.page_length = self.settings.products_per_page or 20
self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route'] self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants',
'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage']
self.filters = [] self.filters = []
self.or_filters = [['show_in_website', '=', 1]] self.or_filters = [['show_in_website', '=', 1]]
if not self.settings.get('hide_variants'): if not self.settings.get('hide_variants'):
self.or_filters.append(['show_variant_in_website', '=', 1]) self.or_filters.append(['show_variant_in_website', '=', 1])
def query(self, attributes=None, fields=None, search_term=None, start=0): def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
"""Summary """Summary
Args: Args:
@ -44,6 +45,15 @@ class ProductQuery:
if search_term: self.build_search_filters(search_term) if search_term: self.build_search_filters(search_term)
result = [] result = []
website_item_groups = []
# if from item group page consider website item group table
if item_group:
website_item_groups = frappe.db.get_all(
"Item",
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
filters=[["Website Item Group", "item_group", "=", item_group]]
)
if attributes: if attributes:
all_items = [] all_items = []
@ -66,7 +76,6 @@ class ProductQuery:
) )
items_dict = {item.name: item for item in items} items_dict = {item.name: item for item in items}
# TODO: Replace Variants by their parent templates
all_items.append(set(items_dict.keys())) all_items.append(set(items_dict.keys()))
@ -78,14 +87,22 @@ class ProductQuery:
filters=self.filters, filters=self.filters,
or_filters=self.or_filters, or_filters=self.or_filters,
start=start, start=start,
limit=self.page_length, limit=self.page_length
order_by="weightage desc"
) )
# Combine results having context of website item groups into item results
if item_group and website_item_groups:
items_list = {row.name for row in result}
for row in website_item_groups:
if row.wig_parent not in items_list:
result.append(row)
result = sorted(result, key=lambda x: x.get("weightage"), reverse=True)
for item in result: for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info: if product_info:
item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
return result return result
@ -99,7 +116,16 @@ class ProductQuery:
if not values: if not values:
continue continue
if isinstance(values, list): # handle multiselect fields in filter addition
meta = frappe.get_meta('Item', cached=True)
df = meta.get_field(field)
if df.fieldtype == 'Table MultiSelect':
child_doctype = df.options
child_meta = frappe.get_meta(child_doctype, cached=True)
fields = child_meta.get("fields")
if fields:
self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
elif isinstance(values, list):
# If value is a list use `IN` query # If value is a list use `IN` query
self.filters.append([field, 'IN', values]) self.filters.append([field, 'IN', values])
else: else:

View File

@ -581,7 +581,6 @@ def update_billing_percentage(pr_doc, update_modified=True):
@frappe.whitelist() @frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None): def make_purchase_invoice(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.party import get_payment_terms_template from erpnext.accounts.party import get_payment_terms_template
doc = frappe.get_doc('Purchase Receipt', source_name) doc = frappe.get_doc('Purchase Receipt', source_name)
@ -601,11 +600,16 @@ def make_purchase_invoice(source_name, target_doc=None):
def update_item(source_doc, target_doc, source_parent): def update_item(source_doc, target_doc, source_parent):
target_doc.qty, returned_qty = get_pending_qty(source_doc) target_doc.qty, returned_qty = get_pending_qty(source_doc)
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
target_doc.rejected_qty = 0
target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor"))
returned_qty_map[source_doc.name] = returned_qty returned_qty_map[source_doc.name] = returned_qty
def get_pending_qty(item_row): def get_pending_qty(item_row):
pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) qty = item_row.qty
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
qty = item_row.received_qty
pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
returned_qty = flt(returned_qty_map.get(item_row.name, 0)) returned_qty = flt(returned_qty_map.get(item_row.name, 0))
if returned_qty: if returned_qty:
if returned_qty >= pending_qty: if returned_qty >= pending_qty:

View File

@ -421,11 +421,18 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(return_pr_2.items[0].qty, -3) self.assertEqual(return_pr_2.items[0].qty, -3)
# Make PI against unreturned amount # Make PI against unreturned amount
buying_settings = frappe.get_single("Buying Settings")
buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0
buying_settings.save()
pi = make_purchase_invoice(pr.name) pi = make_purchase_invoice(pr.name)
pi.submit() pi.submit()
self.assertEqual(pi.items[0].qty, 3) self.assertEqual(pi.items[0].qty, 3)
buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 1
buying_settings.save()
pr.load_from_db() pr.load_from_db()
# PR should be completed on billing all unreturned amount # PR should be completed on billing all unreturned amount
self.assertEqual(pr.items[0].billed_amt, 150) self.assertEqual(pr.items[0].billed_amt, 150)
@ -767,8 +774,8 @@ class TestPurchaseReceipt(unittest.TestCase):
pr1.items[0].purchase_receipt_item = pr.items[0].name pr1.items[0].purchase_receipt_item = pr.items[0].name
pr1.submit() pr1.submit()
pi = make_purchase_invoice(pr.name) pi1 = make_purchase_invoice(pr.name)
self.assertEqual(pi.items[0].qty, 3) self.assertEqual(pi1.items[0].qty, 3)
pr1.cancel() pr1.cancel()
pr.reload() pr.reload()

View File

@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Issues"] = {
get_chart_data: function(_columns, result) { get_chart_data: function(_columns, result) {
return { return {
data: { data: {
labels: result.map(d => d[0]), labels: result.map(d => d.creation_date),
datasets: [{ datasets: [{
name: 'First Response Time', name: 'First Response Time',
values: result.map(d => d[1]) values: result.map(d => d.first_response_time)
}] }]
}, },
type: "line", type: "line",
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Issues"] = {
hide_days: 0, hide_days: 0,
hide_seconds: 0 hide_seconds: 0
}; };
value = frappe.utils.get_formatted_duration(d, duration_options); return frappe.utils.get_formatted_duration(d, duration_options);
return value;
} }
} }
} }

View File

@ -99,6 +99,7 @@ frappe.ready(() => {
fieldname: 'country', fieldname: 'country',
fieldtype: 'Link', fieldtype: 'Link',
options: 'Country', options: 'Country',
only_select: true,
reqd: 1 reqd: 1
}, },
{ {