Merge branch 'develop' into pr-item-gl-fix
This commit is contained in:
commit
c10fea8a59
@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.5.1'
|
||||
__version__ = '13.5.2'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
|
||||
if address and frappe.db.get_value('Dynamic Link',
|
||||
{'parent': address, 'link_name': company}):
|
||||
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 {}
|
||||
|
||||
|
@ -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,
|
||||
total_days, total_booking_days, account_currency)
|
||||
|
||||
if not amount:
|
||||
return
|
||||
|
||||
if via_journal_entry:
|
||||
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)
|
||||
|
@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
||||
doc: frm.doc,
|
||||
btn: $(btn_primary),
|
||||
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]));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,7 +216,8 @@ def start_import(invoices):
|
||||
return names
|
||||
|
||||
def publish(index, total, doctype):
|
||||
if total < 5: return
|
||||
if total < 50:
|
||||
return
|
||||
frappe.publish_realtime(
|
||||
"opening_invoice_creation_progress",
|
||||
dict(
|
||||
@ -241,4 +242,3 @@ def get_temporary_opening_account(company=None):
|
||||
|
||||
return accounts[0].name
|
||||
|
||||
|
||||
|
@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
});
|
||||
}
|
||||
|
||||
company() {
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
}
|
||||
|
||||
onload() {
|
||||
super.onload();
|
||||
|
||||
@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
frm: frm,
|
||||
freeze_message: __("Creating Purchase Receipt ...")
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
})
|
||||
|
@ -168,21 +168,24 @@ def get_columns(filters):
|
||||
"label": _("Income"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 305
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname": "expense",
|
||||
"label": _("Expense"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 305
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname": "gross_profit_loss",
|
||||
"label": _("Gross Profit / Loss"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 307
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -9,13 +9,14 @@
|
||||
"supp_master_name",
|
||||
"supplier_group",
|
||||
"buying_price_list",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"column_break_3",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"maintain_same_rate",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
@ -108,6 +109,13 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"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",
|
||||
@ -115,7 +123,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-04 20:01:44.087066",
|
||||
"modified": "2021-06-23 19:40:00.120822",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
@ -828,8 +828,14 @@ class AccountsController(TransactionBase):
|
||||
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():
|
||||
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))
|
||||
if self.doctype != "Purchase Invoice":
|
||||
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):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
|
@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabEmployee`
|
||||
where status = 'Active'
|
||||
where status in ('Active', 'Suspended')
|
||||
and docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or employee_name like %(txt)s)
|
||||
|
@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
||||
get_chart_data: function (_columns, result) {
|
||||
return {
|
||||
data: {
|
||||
labels: result.map(d => d[0]),
|
||||
labels: result.map(d => d.creation_date),
|
||||
datasets: [{
|
||||
name: "First Response Time",
|
||||
values: result.map(d => d[1])
|
||||
values: result.map(d => d.first_response_time)
|
||||
}]
|
||||
},
|
||||
type: "line",
|
||||
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
||||
hide_days: 0,
|
||||
hide_seconds: 0
|
||||
};
|
||||
value = frappe.utils.get_formatted_duration(d, duration_options);
|
||||
return value;
|
||||
return frappe.utils.get_formatted_duration(d, duration_options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +207,7 @@
|
||||
"label": "Status",
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Active\nInactive\nLeft",
|
||||
"options": "Active\nInactive\nSuspended\nLeft",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@ -813,7 +813,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-06-12 11:31:37.730760",
|
||||
"modified": "2021-06-17 11:31:37.730760",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
|
@ -4,7 +4,7 @@
|
||||
from __future__ import unicode_literals
|
||||
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 import throw, _, scrub
|
||||
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 erpnext.utilities.transaction_base import delete_events
|
||||
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 EmployeeLeftValidationError(frappe.ValidationError): pass
|
||||
@ -37,7 +36,7 @@ class Employee(NestedSet):
|
||||
|
||||
def validate(self):
|
||||
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.set_employee_name()
|
||||
|
@ -7,7 +7,8 @@ def get_data():
|
||||
'heatmap_message': _('This is based on the attendance of this Employee'),
|
||||
'fieldname': 'employee',
|
||||
'non_standard_fieldnames': {
|
||||
'Bank Account': 'party'
|
||||
'Bank Account': 'party',
|
||||
'Employee Grievance': 'raised_by'
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
@ -20,7 +21,7 @@ def get_data():
|
||||
},
|
||||
{
|
||||
'label': _('Lifecycle'),
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
|
||||
},
|
||||
{
|
||||
'label': _('Shift'),
|
||||
|
@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
|
||||
filters: [["status","=", "Active"]],
|
||||
get_indicator: function(doc) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
0
erpnext/hr/doctype/employee_grievance/__init__.py
Normal file
0
erpnext/hr/doctype/employee_grievance/__init__.py
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal 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
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal file
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal 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
|
||||
}
|
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal file
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal 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"))
|
||||
)
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
@ -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
|
||||
|
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal file
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal 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"
|
||||
}
|
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal 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
|
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal 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
|
@ -2,7 +2,7 @@
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.listview_settings['Job Applicant'] = {
|
||||
add_fields: ["company", "designation", "job_applicant", "status"],
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Accepted") {
|
||||
return [__(doc.status), "green", "status,=," + doc.status];
|
||||
|
@ -110,6 +110,7 @@
|
||||
"label": "Allocation"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"bold": 1,
|
||||
"fieldname": "new_leaves_allocated",
|
||||
"fieldtype": "Float",
|
||||
@ -235,7 +236,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-14 15:28:26.335104",
|
||||
"modified": "2021-06-03 15:28:26.335104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Allocation",
|
||||
@ -277,4 +278,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"timeline_field": "employee"
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
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_application.leave_application import get_approved_leaves_for_period
|
||||
|
||||
class OverlapError(frappe.ValidationError): pass
|
||||
class BackDatedAllocationError(frappe.ValidationError): pass
|
||||
@ -55,6 +56,43 @@ class LeaveAllocation(Document):
|
||||
if self.carry_forward:
|
||||
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):
|
||||
allocations = frappe.db.get_list("Leave Allocation", filters = {
|
||||
"docstatus": 1,
|
||||
@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date):
|
||||
|
||||
def validate_carry_forward(leave_type):
|
||||
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))
|
||||
|
@ -1,10 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import erpnext
|
||||
import unittest
|
||||
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_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation
|
||||
|
||||
class TestLeaveAllocation(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -164,6 +164,53 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
leave_allocation.cancel()
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
|
||||
|
@ -103,4 +103,4 @@ var set_total_estimated_budget = function(frm) {
|
||||
})
|
||||
frm.set_value('total_estimated_budget', estimated_budget);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ class StaffingPlan(Document):
|
||||
|
||||
detail.total_estimated_cost = 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)
|
||||
|
||||
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 \
|
||||
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} \
|
||||
for {2} as per staffing plan {3} for parent company {4}."
|
||||
.format(cint(parent_plan_details[0].vacancies),
|
||||
for {2} as per staffing plan {3} for parent company {4}.").format(
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
parent_plan_details[0].total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_plan_details[0].name,
|
||||
parent_company)), ParentCompanyError)
|
||||
parent_company), ParentCompanyError)
|
||||
|
||||
#Get vacanices already planned for all companies down the hierarchy of Parent Company
|
||||
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(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}. \
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
|
||||
.format(cint(all_sibling_details.vacancies),
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
|
||||
cint(all_sibling_details.vacancies),
|
||||
all_sibling_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_company,
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
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):
|
||||
#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 \
|
||||
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}. \
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
|
||||
.format(self.company,
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
|
||||
self.company,
|
||||
cint(children_details.vacancies),
|
||||
children_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
|
||||
frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
|
||||
|
||||
@frappe.whitelist()
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
@ -11,8 +11,8 @@
|
||||
"event": "Submit",
|
||||
"idx": 0,
|
||||
"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>",
|
||||
"modified": "2021-05-24 16:29:13.165930",
|
||||
"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-06-16 14:08:12.933367",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Training Scheduled",
|
||||
|
@ -24,19 +24,19 @@
|
||||
{% set start = frappe.utils.get_datetime(doc.start_time) %}
|
||||
{% set end = frappe.utils.get_datetime(doc.end_time) %}
|
||||
{% if start.date() == end.date() %}
|
||||
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
|
||||
<li>
|
||||
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
|
||||
</li>
|
||||
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
|
||||
<li>
|
||||
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
|
||||
</li>
|
||||
<li>{{_("End Time")}}: <b>{{ end.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>
|
||||
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
|
||||
{% 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 %}
|
||||
<li>Note: This Training Event is mandatory</li>
|
||||
<li>{{ _("Note: This Training Event is mandatory") }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
@ -44,4 +44,4 @@
|
||||
<td width="15"></td>
|
||||
</tr>
|
||||
<tr height="10"></tr>
|
||||
</table>
|
||||
</table>
|
||||
|
@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
|
||||
is_carry_forward, is_expired
|
||||
FROM `tabLeave Ledger Entry`
|
||||
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
|
||||
OR to_date between %(from_date)s AND %(to_date)s
|
||||
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
|
||||
|
@ -153,6 +153,24 @@
|
||||
"onboard": 0,
|
||||
"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",
|
||||
"hidden": 0,
|
||||
@ -823,7 +841,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-04-26 13:36:15.413819",
|
||||
"modified": "2021-05-13 17:19:40.524444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR",
|
||||
|
@ -704,6 +704,8 @@ erpnext.work_order = {
|
||||
stop_work_order: function(frm, status) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
|
||||
freeze: true,
|
||||
freeze_message: __("Updating Work Order status"),
|
||||
args: {
|
||||
work_order: frm.doc.name,
|
||||
status: status
|
||||
|
@ -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.add_doctype_to_sla #14-06-2021
|
||||
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
|
||||
|
@ -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()
|
@ -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) {
|
||||
@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', {
|
||||
},
|
||||
|
||||
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;
|
||||
let filters = {company: frm.doc.company};
|
||||
if (frm.doc.type) {
|
||||
filters.type = frm.doc.type;
|
||||
}
|
||||
frm.set_query("salary_component", function() {
|
||||
return {
|
||||
filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company}
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -11,6 +11,7 @@ from frappe import _
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
||||
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):
|
||||
def onload(self):
|
||||
@ -41,7 +42,7 @@ class PayrollEntry(Document):
|
||||
emp_with_sal_slip.append(employee_details.employee)
|
||||
|
||||
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):
|
||||
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||
@ -211,7 +212,7 @@ class PayrollEntry(Document):
|
||||
return account_dict
|
||||
|
||||
def make_accrual_jv_entry(self):
|
||||
self.check_permission('write')
|
||||
self.check_permission("write")
|
||||
earnings = self.get_salary_component_total(component_type = "earnings") or {}
|
||||
deductions = self.get_salary_component_total(component_type = "deductions") or {}
|
||||
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")
|
||||
|
||||
if earnings or deductions:
|
||||
journal_entry = frappe.new_doc('Journal Entry')
|
||||
journal_entry.voucher_type = 'Journal Entry'
|
||||
journal_entry.user_remark = _('Accrual Journal Entry for salaries from {0} to {1}')\
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Journal Entry"
|
||||
journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\
|
||||
.format(self.start_date, self.end_date)
|
||||
journal_entry.company = self.company
|
||||
journal_entry.posting_date = self.posting_date
|
||||
accounting_dimensions = get_accounting_dimensions() or []
|
||||
|
||||
accounts = []
|
||||
currencies = []
|
||||
@ -236,37 +238,34 @@ class PayrollEntry(Document):
|
||||
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)
|
||||
payable_amount += flt(amount, precision)
|
||||
accounts.append({
|
||||
accounts.append(self.update_accounting_dimensions({
|
||||
"account": acc_cc[0],
|
||||
"debit_in_account_currency": flt(amt, precision),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"party_type": '',
|
||||
"cost_center": acc_cc[1] or self.cost_center,
|
||||
"project": self.project
|
||||
})
|
||||
}, accounting_dimensions))
|
||||
|
||||
# Deductions
|
||||
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)
|
||||
payable_amount -= flt(amount, precision)
|
||||
accounts.append({
|
||||
accounts.append(self.update_accounting_dimensions({
|
||||
"account": acc_cc[0],
|
||||
"credit_in_account_currency": flt(amt, precision),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"cost_center": acc_cc[1] or self.cost_center,
|
||||
"party_type": '',
|
||||
"project": self.project
|
||||
})
|
||||
}, accounting_dimensions))
|
||||
|
||||
# Payable amount
|
||||
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,
|
||||
"credit_in_account_currency": flt(payable_amt, precision),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"party_type": '',
|
||||
"cost_center": self.cost_center
|
||||
})
|
||||
}, accounting_dimensions))
|
||||
|
||||
journal_entry.set("accounts", accounts)
|
||||
if len(currencies) > 1:
|
||||
@ -286,6 +285,12 @@ class PayrollEntry(Document):
|
||||
|
||||
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):
|
||||
conversion_rate = 1
|
||||
exchange_rate = self.exchange_rate
|
||||
|
@ -481,6 +481,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
if not salary_structure:
|
||||
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
|
||||
|
||||
|
||||
employee = frappe.db.get_value("Employee", {"user_id": user})
|
||||
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})})
|
||||
|
@ -124,8 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
|
||||
"doctype": "Salary Structure",
|
||||
"name": salary_structure,
|
||||
"company": company or erpnext.get_default_company(),
|
||||
"earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"deductions": make_deduction_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(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"payroll_frequency": payroll_frequency,
|
||||
"payment_account": get_random("Account", filters={'account_currency': currency}),
|
||||
"currency": currency
|
||||
|
@ -41,6 +41,30 @@ class TestProductConfigurator(unittest.TestCase):
|
||||
"show_variant_in_website": 1
|
||||
}).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):
|
||||
template_items = frappe.get_all('Item', {'show_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']
|
||||
})
|
||||
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()
|
@ -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") &&
|
||||
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
|
||||
erpnext.utils.get_shipping_address(this.frm, function(){
|
||||
|
@ -274,9 +274,9 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
|
||||
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.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)) {
|
||||
if (callback) {
|
||||
return callback();
|
||||
|
@ -467,11 +467,15 @@ body.product-page {
|
||||
|
||||
.btn-change-address {
|
||||
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 {
|
||||
.card-body {
|
||||
padding: var(--padding-sm);
|
||||
|
@ -241,8 +241,8 @@ erpnext.PointOfSale.Controller = class {
|
||||
events: {
|
||||
get_frm: () => this.frm,
|
||||
|
||||
cart_item_clicked: (item_code, batch_no, uom, rate) => {
|
||||
const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
|
||||
cart_item_clicked: (item) => {
|
||||
const item_row = this.get_item_from_frm(item);
|
||||
this.item_details.toggle_item_details_section(item_row);
|
||||
},
|
||||
|
||||
@ -273,17 +273,15 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.cart.toggle_numpad(minimize);
|
||||
},
|
||||
|
||||
form_updated: (cdt, cdn, fieldname, value) => {
|
||||
const item_row = frappe.model.get_doc(cdt, cdn);
|
||||
if (item_row && item_row[fieldname] != value) {
|
||||
|
||||
const { item_code, batch_no, uom, rate } = this.item_details.current_item;
|
||||
const event = {
|
||||
field: fieldname,
|
||||
form_updated: (item, field, value) => {
|
||||
const item_row = frappe.model.get_doc(item.doctype, item.name);
|
||||
if (item_row && item_row[field] != value) {
|
||||
const args = {
|
||||
field,
|
||||
value,
|
||||
item: { item_code, batch_no, uom, rate }
|
||||
}
|
||||
return this.on_cart_update(event)
|
||||
item: this.item_details.current_item
|
||||
};
|
||||
return this.on_cart_update(args);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
@ -300,19 +298,18 @@ erpnext.PointOfSale.Controller = class {
|
||||
set_value_in_current_cart_item: (selector, value) => {
|
||||
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
|
||||
// for each unique batch new item row is added in the form & cart
|
||||
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.item_code === item_code && i.batch_no === batch_no);
|
||||
const item_to_clone = this.frm.doc.items.find(i => i.name == item.name);
|
||||
const new_row = this.frm.add_child("items", { ...item_to_clone });
|
||||
// update new serialno and batch
|
||||
new_row.batch_no = batch;
|
||||
new_row.serial_no = batch_serial_map[batch].join(`\n`);
|
||||
new_row.qty = batch_serial_map[batch].length;
|
||||
this.frm.doc.items.forEach(row => {
|
||||
if (item_code === row.item_code) {
|
||||
if (item.item_code === row.item_code) {
|
||||
this.update_cart_html(row);
|
||||
}
|
||||
});
|
||||
@ -321,8 +318,8 @@ erpnext.PointOfSale.Controller = class {
|
||||
remove_item_from_cart: () => this.remove_item_from_cart(),
|
||||
get_item_stock_map: () => this.item_stock_map,
|
||||
close_item_details: () => {
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
this.cart.prev_action = undefined;
|
||||
this.item_details.toggle_item_details_section(null);
|
||||
this.cart.prev_action = null;
|
||||
this.cart.toggle_item_highlight();
|
||||
},
|
||||
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;
|
||||
try {
|
||||
let { field, value, item } = args;
|
||||
const { item_code, batch_no, serial_no, uom, rate } = item;
|
||||
item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
|
||||
item_row = this.get_item_from_frm(item);
|
||||
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) {
|
||||
item_selected_from_selector && (value = item_row.qty + flt(value))
|
||||
|
||||
field === 'qty' && (value = flt(value));
|
||||
if (item_row_exists) {
|
||||
if (field === 'qty')
|
||||
value = flt(value);
|
||||
|
||||
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;
|
||||
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);
|
||||
this.update_cart_html(item_row);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (!this.frm.doc.customer) {
|
||||
frappe.dom.unfreeze();
|
||||
frappe.show_alert({
|
||||
message: __('You must select a customer before adding an item.'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
if (!this.frm.doc.customer)
|
||||
return this.raise_customer_selection_alert();
|
||||
|
||||
const { item_code, batch_no, serial_no, rate } = item;
|
||||
|
||||
if (!item_code)
|
||||
return;
|
||||
}
|
||||
if (!item_code) return;
|
||||
|
||||
item_selected_from_selector && (value = flt(value))
|
||||
|
||||
const args = { item_code, batch_no, rate, [field]: value };
|
||||
const new_item = { item_code, batch_no, rate, [field]: value };
|
||||
|
||||
if (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)
|
||||
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.item_details.$component.is(':visible') && this.edit_item_details_of(item_row);
|
||||
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
|
||||
if (this.item_details.$component.is(':visible'))
|
||||
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) {
|
||||
@ -570,14 +567,33 @@ erpnext.PointOfSale.Controller = class {
|
||||
}
|
||||
}
|
||||
|
||||
get_item_from_frm(item_code, batch_no, uom, rate) {
|
||||
const has_batch_no = batch_no;
|
||||
return 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)
|
||||
);
|
||||
raise_customer_selection_alert() {
|
||||
frappe.dom.unfreeze();
|
||||
frappe.show_alert({
|
||||
message: __('You must select a customer before adding an item.'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
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) {
|
||||
@ -585,9 +601,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
}
|
||||
|
||||
is_current_item_being_edited(item_row) {
|
||||
const { item_code, batch_no } = this.item_details.current_item;
|
||||
|
||||
return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true;
|
||||
return item_row.name == this.item_details.current_item.name;
|
||||
}
|
||||
|
||||
update_cart_html(item_row, remove_item) {
|
||||
@ -669,7 +683,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
update_item_field(value, field_or_action) {
|
||||
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') {
|
||||
this.remove_item_from_cart();
|
||||
} else {
|
||||
@ -688,7 +702,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
.then(() => {
|
||||
frappe.model.clear_doc(doctype, name);
|
||||
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();
|
||||
})
|
||||
.catch(e => console.log(e));
|
||||
|
@ -181,11 +181,8 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
me.$totals_section.find(".edit-cart-btn").click();
|
||||
}
|
||||
|
||||
const item_code = unescape($cart_item.attr('data-item-code'));
|
||||
const batch_no = unescape($cart_item.attr('data-batch-no'));
|
||||
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);
|
||||
const item_row_name = unescape($cart_item.attr('data-row-name'));
|
||||
me.events.cart_item_clicked({ name: item_row_name });
|
||||
this.numpad_value = '';
|
||||
});
|
||||
|
||||
@ -521,25 +518,14 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
}
|
||||
|
||||
get_cart_item({ item_code, batch_no, uom, rate }) {
|
||||
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
|
||||
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}`;
|
||||
|
||||
get_cart_item({ name }) {
|
||||
const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`;
|
||||
return this.$cart_items_wrapper.find(item_selector);
|
||||
}
|
||||
|
||||
get_item_from_frm(item) {
|
||||
const doc = this.events.get_frm().doc;
|
||||
const { item_code, batch_no, uom, rate } = item;
|
||||
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);
|
||||
return doc.items.find(i => i.name == item.name);
|
||||
}
|
||||
|
||||
update_item_html(item, remove_item) {
|
||||
@ -564,10 +550,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
if (!$item_to_update.length) {
|
||||
this.$cart_items_wrapper.append(
|
||||
`<div class="cart-item-wrapper"
|
||||
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="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
|
||||
<div class="seperator"></div>`
|
||||
)
|
||||
$item_to_update = this.get_cart_item(item_data);
|
||||
@ -642,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
function get_item_image_html() {
|
||||
const { image, item_name } = item_data;
|
||||
if (image) {
|
||||
if (!me.hide_images && image) {
|
||||
return `
|
||||
<div class="item-image">
|
||||
<img
|
||||
|
@ -2,6 +2,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
constructor({ wrapper, events, settings }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
this.hide_images = settings.hide_images;
|
||||
this.allow_rate_change = settings.allow_rate_change;
|
||||
this.allow_discount_change = settings.allow_discount_change;
|
||||
this.current_item = {};
|
||||
@ -54,36 +55,28 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.$dicount_section = this.$component.find('.discount-section');
|
||||
}
|
||||
|
||||
has_item_has_changed(item) {
|
||||
const { item_code, batch_no, uom, rate } = this.current_item;
|
||||
const item_code_is_same = item && item_code === item.item_code;
|
||||
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;
|
||||
compare_with_current_item(item) {
|
||||
// returns true if `item` is currently being edited
|
||||
return item && item.name == this.current_item.name;
|
||||
}
|
||||
|
||||
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);
|
||||
this.toggle_component(this.item_has_changed);
|
||||
// if item is null or highlighted cart item is clicked twice
|
||||
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.item_meta = frappe.get_meta(this.doctype);
|
||||
this.name = item.name;
|
||||
this.item_row = item;
|
||||
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_discount_dom(item);
|
||||
@ -132,7 +125,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.$item_name.html(item_name);
|
||||
this.$item_description.html(get_description_html());
|
||||
this.$item_price.html(format_currency(price_list_rate, this.currency));
|
||||
if (image) {
|
||||
if (!this.hide_images && image) {
|
||||
this.$item_image.html(
|
||||
`<img
|
||||
onerror="cur_pos.item_details.handle_broken_image(this)"
|
||||
@ -180,7 +173,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
df: {
|
||||
...field_meta,
|
||||
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`),
|
||||
@ -218,22 +211,17 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
bind_custom_control_change_event() {
|
||||
const me = this;
|
||||
if (this.rate_control) {
|
||||
if (this.allow_rate_change) {
|
||||
this.rate_control.df.onchange = function() {
|
||||
if (this.value || flt(this.value) === 0) {
|
||||
me.events.set_value_in_current_cart_item('rate', this.value);
|
||||
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
const doc = me.events.get_frm().doc;
|
||||
me.$item_price.html(format_currency(item_row.rate, doc.currency));
|
||||
me.render_discount_dom(item_row);
|
||||
});
|
||||
me.current_item.rate = this.value;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.rate_control.df.read_only = 1;
|
||||
}
|
||||
this.rate_control.df.onchange = function() {
|
||||
if (this.value || flt(this.value) === 0) {
|
||||
me.events.form_updated(me.current_item, 'rate', this.value).then(() => {
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
const doc = me.events.get_frm().doc;
|
||||
me.$item_price.html(format_currency(item_row.rate, doc.currency));
|
||||
me.render_discount_dom(item_row);
|
||||
});
|
||||
}
|
||||
};
|
||||
this.rate_control.df.read_only = !this.allow_rate_change;
|
||||
this.rate_control.refresh();
|
||||
}
|
||||
|
||||
@ -246,7 +234,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.warehouse_control.df.reqd = 1;
|
||||
this.warehouse_control.df.onchange = function() {
|
||||
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();
|
||||
const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
|
||||
if (available_qty === undefined) {
|
||||
@ -278,7 +266,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.serial_no_control.df.reqd = 1;
|
||||
this.serial_no_control.df.onchange = async function() {
|
||||
!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();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
if (this.uom_control) {
|
||||
this.uom_control.df.onchange = function() {
|
||||
me.events.set_value_in_current_cart_item('uom', this.value);
|
||||
me.events.form_updated(me.doctype, me.name, 'uom', this.value);
|
||||
me.current_item.uom = this.value;
|
||||
me.events.form_updated(me.current_item, 'uom', this.value);
|
||||
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
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) => {
|
||||
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);
|
||||
cur_pos.update_cart_html(item_row);
|
||||
}
|
||||
@ -337,7 +318,9 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
fields: ["batch_no", "name"]
|
||||
});
|
||||
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];
|
||||
return acc;
|
||||
}, {});
|
||||
@ -353,12 +336,10 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
if (serial_nos_belongs_to_other_batch) {
|
||||
this.serial_no_control.set_value(batch_serial_nos);
|
||||
this.qty_control.set_value(batch_serial_map[batch_no].length);
|
||||
}
|
||||
|
||||
delete batch_serial_map[batch_no];
|
||||
|
||||
if (serial_nos_belongs_to_other_batch)
|
||||
delete batch_serial_map[batch_no];
|
||||
this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,7 +232,11 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
uom = uom === "undefined" ? undefined : uom;
|
||||
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('');
|
||||
});
|
||||
|
||||
|
@ -59,7 +59,7 @@ def get_data(conditions, filters):
|
||||
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||
soi.qty, soi.delivered_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.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
|
||||
|
@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
field_filters['item_group'] = self.name
|
||||
|
||||
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)
|
||||
|
||||
|
@ -22,12 +22,15 @@ class ProductFiltersBuilder:
|
||||
|
||||
filter_data = []
|
||||
for df in fields:
|
||||
filters = {}
|
||||
filters, or_filters = {}, []
|
||||
if df.fieldtype == "Link":
|
||||
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:
|
||||
doctype = df.get_link_doctype()
|
||||
|
||||
@ -44,7 +47,9 @@ class ProductFiltersBuilder:
|
||||
values = [d.name for d in frappe.get_all(doctype, filters)]
|
||||
|
||||
# Remove None
|
||||
values = values.remove(None) if None in values else values
|
||||
if None in values:
|
||||
values.remove(None)
|
||||
|
||||
if values:
|
||||
filter_data.append([df, values])
|
||||
|
||||
@ -61,14 +66,18 @@ class ProductFiltersBuilder:
|
||||
for attr_doc in attribute_docs:
|
||||
selected_attributes = []
|
||||
for attr in attr_doc.item_attribute_values:
|
||||
or_filters = []
|
||||
filters= [
|
||||
["Item Variant Attribute", "attribute", "=", attr.parent],
|
||||
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
|
||||
]
|
||||
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)
|
||||
|
||||
if selected_attributes:
|
||||
|
@ -22,13 +22,14 @@ class ProductQuery:
|
||||
self.settings = frappe.get_doc("Products Settings")
|
||||
self.cart_settings = frappe.get_doc("Shopping Cart Settings")
|
||||
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.or_filters = [['show_in_website', '=', 1]]
|
||||
if not self.settings.get('hide_variants'):
|
||||
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
|
||||
|
||||
Args:
|
||||
@ -44,6 +45,15 @@ class ProductQuery:
|
||||
if search_term: self.build_search_filters(search_term)
|
||||
|
||||
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:
|
||||
all_items = []
|
||||
@ -66,7 +76,6 @@ class ProductQuery:
|
||||
)
|
||||
|
||||
items_dict = {item.name: item for item in items}
|
||||
# TODO: Replace Variants by their parent templates
|
||||
|
||||
all_items.append(set(items_dict.keys()))
|
||||
|
||||
@ -78,14 +87,22 @@ class ProductQuery:
|
||||
filters=self.filters,
|
||||
or_filters=self.or_filters,
|
||||
start=start,
|
||||
limit=self.page_length,
|
||||
order_by="weightage desc"
|
||||
limit=self.page_length
|
||||
)
|
||||
|
||||
# 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:
|
||||
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('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
|
||||
|
||||
@ -99,7 +116,16 @@ class ProductQuery:
|
||||
if not values:
|
||||
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
|
||||
self.filters.append([field, 'IN', values])
|
||||
else:
|
||||
|
@ -581,7 +581,6 @@ def update_billing_percentage(pr_doc, update_modified=True):
|
||||
|
||||
@frappe.whitelist()
|
||||
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
|
||||
|
||||
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):
|
||||
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"))
|
||||
returned_qty_map[source_doc.name] = returned_qty
|
||||
|
||||
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))
|
||||
if returned_qty:
|
||||
if returned_qty >= pending_qty:
|
||||
|
@ -421,11 +421,18 @@ class TestPurchaseReceipt(unittest.TestCase):
|
||||
self.assertEqual(return_pr_2.items[0].qty, -3)
|
||||
|
||||
# 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.submit()
|
||||
|
||||
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 should be completed on billing all unreturned amount
|
||||
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.submit()
|
||||
|
||||
pi = make_purchase_invoice(pr.name)
|
||||
self.assertEqual(pi.items[0].qty, 3)
|
||||
pi1 = make_purchase_invoice(pr.name)
|
||||
self.assertEqual(pi1.items[0].qty, 3)
|
||||
|
||||
pr1.cancel()
|
||||
pr.reload()
|
||||
|
@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Issues"] = {
|
||||
get_chart_data: function(_columns, result) {
|
||||
return {
|
||||
data: {
|
||||
labels: result.map(d => d[0]),
|
||||
labels: result.map(d => d.creation_date),
|
||||
datasets: [{
|
||||
name: 'First Response Time',
|
||||
values: result.map(d => d[1])
|
||||
values: result.map(d => d.first_response_time)
|
||||
}]
|
||||
},
|
||||
type: "line",
|
||||
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Issues"] = {
|
||||
hide_days: 0,
|
||||
hide_seconds: 0
|
||||
};
|
||||
value = frappe.utils.get_formatted_duration(d, duration_options);
|
||||
return value;
|
||||
return frappe.utils.get_formatted_duration(d, duration_options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +99,7 @@ frappe.ready(() => {
|
||||
fieldname: 'country',
|
||||
fieldtype: 'Link',
|
||||
options: 'Country',
|
||||
only_select: true,
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user