Merge branch 'develop' into asset-repair-refactor

This commit is contained in:
Saqib 2021-06-28 11:07:58 +05:30 committed by GitHub
commit 4b69563a56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 1505 additions and 379 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

@ -86,7 +86,7 @@ def resolve_dunning(doc, state):
for reference in doc.references: for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
dunnings = frappe.get_list('Dunning', filters={ dunnings = frappe.get_list('Dunning', filters={
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}) 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
for dunning in dunnings: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')

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

@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);

View File

@ -690,7 +690,7 @@
"options": "Account" "options": "Account"
}, },
{ {
"depends_on": "eval:doc.received_amount", "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax", "fieldname": "received_amount_after_tax",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Received Amount After Tax", "label": "Received Amount After Tax",
@ -707,7 +707,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-06-09 11:55:04.215050", "modified": "2021-06-22 20:37:06.154206",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@ -706,7 +706,7 @@ class PaymentEntry(AccountsController):
if account_currency != self.company_currency: if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
if self.payment_type == 'Pay': if self.payment_type in ('Pay', 'Internal Transfer'):
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
elif self.payment_type == 'Receive': elif self.payment_type == 'Receive':
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
@ -761,7 +761,7 @@ class PaymentEntry(AccountsController):
return self.advance_tax_account return self.advance_tax_account
elif self.payment_type == 'Receive': elif self.payment_type == 'Receive':
return self.paid_from return self.paid_from
elif self.payment_type == 'Pay': elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_to return self.paid_to
def update_advance_paid(self): def update_advance_paid(self):

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

@ -966,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase):
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate()) update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
# Create Purchase Order with TDS applied # Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000) po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
po.apply_tds = 1 po.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save() po.save()
@ -1002,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase):
# Create Purchase Invoice against Purchase Order # Create Purchase Invoice against Purchase Order
purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice = get_mapped_purchase_invoice(po.name)
purchase_invoice.allocate_advances_automatically = 1 purchase_invoice.allocate_advances_automatically = 1
purchase_invoice.items[0].item_code = '_Test Non Stock Item'
purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC' purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
purchase_invoice.save() purchase_invoice.save()
purchase_invoice.submit() purchase_invoice.submit()

View File

@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
def check_if_in_list(gle, gl_map, dimensions=None): def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type', account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
'cost_center', 'project'] 'cost_center', 'project', 'voucher_detail_no']
if dimensions: if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions account_head_fieldnames = account_head_fieldnames + dimensions

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

@ -99,9 +99,10 @@ def validate_returned_items(doc):
frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}")
.format(d.idx, s, doc.doctype, doc.return_against)) .format(d.idx, s, doc.doctype, doc.return_against))
if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \ if (warehouse_mandatory and not d.get("warehouse") and
and not d.get("warehouse"): frappe.db.get_value("Item", d.item_code, "is_stock_item")
frappe.throw(_("Warehouse is mandatory")) ):
frappe.throw(_("Warehouse is mandatory"))
items_returned = True items_returned = True
@ -462,4 +463,4 @@ def get_returned_serial_nos(child_doc, parent_doc):
for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters):
serial_nos.extend(get_serial_nos(row.serial_no)) serial_nos.extend(get_serial_nos(row.serial_no))
return serial_nos return serial_nos

View File

@ -330,9 +330,15 @@ class SellingController(StockController):
# For internal transfers use incoming rate as the valuation rate # For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer(): if self.is_internal_transfer():
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) if d.doctype == "Packed Item":
if d.rate != rate: incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate'))
d.rate = rate if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if d.rate != rate:
d.rate = rate
d.discount_percentage = 0 d.discount_percentage = 0
d.discount_amount = 0 d.discount_amount = 0
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")

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

@ -1,7 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from typing import List
from collections import deque
import frappe, erpnext import frappe, erpnext
from frappe.utils import cint, cstr, flt, today from frappe.utils import cint, cstr, flt, today
from frappe import _ from frappe import _
@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc
import functools import functools
from six import string_types
from operator import itemgetter from operator import itemgetter
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
} }
class BOMTree:
"""Full tree representation of a BOM"""
# specifying the attributes to save resources
# ref: https://docs.python.org/3/reference/datamodel.html#slots
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None:
self.name = name # name of node, BOM number if is_bom else item_code
self.child_items: List["BOMTree"] = [] # list of child items
self.is_bom = is_bom # true if the node is a BOM and not a leaf item
self.item_code: str = None # item_code associated with node
self.qty = qty # required unit quantity to make one unit of parent item.
self.exploded_qty = exploded_qty # total exploded qty required for making root of tree.
if not self.is_bom:
self.item_code = self.name
else:
self.__create_tree()
def __create_tree(self):
bom = frappe.get_cached_doc("BOM", self.name)
self.item_code = bom.item
for item in bom.get("items", []):
qty = item.qty / bom.quantity # quantity per unit
exploded_qty = self.exploded_qty * qty
if item.bom_no:
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
self.child_items.append(child)
else:
self.child_items.append(
BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty)
)
def level_order_traversal(self) -> List["BOMTree"]:
"""Get level order traversal of tree.
E.g. for following tree the traversal will return list of nodes in order from top to bottom.
BOM:
- SubAssy1
- item1
- item2
- SubAssy2
- item3
- item4
returns = [SubAssy1, item1, item2, SubAssy2, item3, item4]
"""
traversal = []
q = deque()
q.append(self)
while q:
node = q.popleft()
for child in node.child_items:
traversal.append(child)
q.append(child)
return traversal
def __str__(self) -> str:
return (
f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}"
f" exploded_qty: {self.exploded_qty}"
)
def __repr__(self, level: int = 0) -> str:
rep = "" * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n"
for child in self.child_items:
rep += child.__repr__(level=level + 1)
return rep
class BOM(WebsiteGenerator): class BOM(WebsiteGenerator):
website = frappe._dict( website = frappe._dict(
# page_title_field = "item_name", # page_title_field = "item_name",
@ -81,7 +153,7 @@ class BOM(WebsiteGenerator):
self.validate_operations() self.validate_operations()
self.calculate_cost() self.calculate_cost()
self.update_stock_qty() self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, save=False) self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
def get_context(self, context): def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }] context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@ -152,7 +224,7 @@ class BOM(WebsiteGenerator):
if not args: if not args:
args = frappe.form_dict.get('args') args = frappe.form_dict.get('args')
if isinstance(args, string_types): if isinstance(args, str):
import json import json
args = json.loads(args) args = json.loads(args)
@ -213,7 +285,7 @@ class BOM(WebsiteGenerator):
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
@frappe.whitelist() @frappe.whitelist()
def update_cost(self, update_parent=True, from_child_bom=False, save=True): def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True):
if self.docstatus == 2: if self.docstatus == 2:
return return
@ -242,7 +314,7 @@ class BOM(WebsiteGenerator):
if self.docstatus == 1: if self.docstatus == 1:
self.flags.ignore_validate_update_after_submit = True self.flags.ignore_validate_update_after_submit = True
self.calculate_cost() self.calculate_cost(update_hour_rate)
if save: if save:
self.db_update() self.db_update()
@ -403,32 +475,47 @@ class BOM(WebsiteGenerator):
bom_list.reverse() bom_list.reverse()
return bom_list return bom_list
def calculate_cost(self): def calculate_cost(self, update_hour_rate = False):
"""Calculate bom totals""" """Calculate bom totals"""
self.calculate_op_cost() self.calculate_op_cost(update_hour_rate)
self.calculate_rm_cost() self.calculate_rm_cost()
self.calculate_sm_cost() self.calculate_sm_cost()
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
def calculate_op_cost(self): def calculate_op_cost(self, update_hour_rate = False):
"""Update workstation rate and calculates totals""" """Update workstation rate and calculates totals"""
self.operating_cost = 0 self.operating_cost = 0
self.base_operating_cost = 0 self.base_operating_cost = 0
for d in self.get('operations'): for d in self.get('operations'):
if d.workstation: if d.workstation:
if not d.hour_rate: self.update_rate_and_time(d, update_hour_rate)
hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate"))
d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate
if d.hour_rate and d.time_in_mins:
d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate)
d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0
d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate)
self.operating_cost += flt(d.operating_cost) self.operating_cost += flt(d.operating_cost)
self.base_operating_cost += flt(d.base_operating_cost) self.base_operating_cost += flt(d.base_operating_cost)
def update_rate_and_time(self, row, update_hour_rate = False):
if not row.hour_rate or update_hour_rate:
hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate"))
row.hour_rate = (hour_rate / flt(self.conversion_rate)
if self.conversion_rate and hour_rate else hour_rate)
if self.routing:
row.time_in_mins = flt(frappe.db.get_value("BOM Operation", {
"workstation": row.workstation,
"operation": row.operation,
"sequence_id": row.sequence_id,
"parent": self.routing
}, ["time_in_mins"]))
if row.hour_rate and row.time_in_mins:
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
if update_hour_rate:
row.db_update()
def calculate_rm_cost(self): def calculate_rm_cost(self):
"""Fetch RM rate as per today's valuation rate and calculate totals""" """Fetch RM rate as per today's valuation rate and calculate totals"""
total_rm_cost = 0 total_rm_cost = 0
@ -585,6 +672,11 @@ class BOM(WebsiteGenerator):
if not d.batch_size or d.batch_size <= 0: if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1 d.batch_size = 1
def get_tree_representation(self) -> BOMTree:
"""Get a complete tree representation preserving order of child items."""
return BOMTree(self.name)
def get_bom_item_rate(args, bom_doc): def get_bom_item_rate(args, bom_doc):
if bom_doc.rm_cost_as_per == 'Valuation Rate': if bom_doc.rm_cost_as_per == 'Valuation Rate':
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
@ -975,7 +1067,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if filters and filters.get("is_stock_item"): if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1 query_filters["is_stock_item"] = 1
return frappe.get_all("Item", return frappe.get_all("Item",
fields = fields, filters=query_filters, fields = fields, filters=query_filters,
or_filters = or_cond_filters, order_by=order_by, or_filters = or_cond_filters, order_by=order_by,

View File

@ -2,14 +2,13 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from collections import deque
import unittest import unittest
import frappe import frappe
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from six import string_types
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.tests.test_subcontracting import set_backflush_based_on from erpnext.tests.test_subcontracting import set_backflush_based_on
@ -123,7 +122,7 @@ class TestBOM(unittest.TestCase):
bom.items[0].conversion_factor = 5 bom.items[0].conversion_factor = 5
bom.insert() bom.insert()
bom.update_cost() bom.update_cost(update_hour_rate = False)
# test amounts in selected currency # test amounts in selected currency
self.assertEqual(bom.items[0].rate, 300) self.assertEqual(bom.items[0].rate, 300)
@ -227,11 +226,88 @@ class TestBOM(unittest.TestCase):
supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEqual(bom_items, supplied_items) self.assertEqual(bom_items, supplied_items)
def test_bom_tree_representation(self):
bom_tree = {
"Assembly": {
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
"SubAssembly2": {"ChildPart3": {}},
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
"ChildPart5": {},
"ChildPart6": {},
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
created_tree = parent_bom.get_tree_representation()
reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
created_order = created_tree.level_order_traversal()
self.assertEqual(len(reqd_order), len(created_order))
for reqd_item, created_item in zip(reqd_order, created_order):
self.assertEqual(reqd_item, created_item.item_code)
def get_default_bom(item_code="_Test FG Item 2"): def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
def level_order_traversal(node):
traversal = []
q = deque()
q.append(node)
while q:
node = q.popleft()
for node_name, subtree in node.items():
traversal.append(node_name)
q.append(subtree)
return traversal
def create_nested_bom(tree, prefix="_Test bom "):
""" Helper function to create a simple nested bom from tree describing item names. (along with required items)
"""
def create_items(bom_tree):
for item_code, subtree in bom_tree.items():
bom_item_code = prefix + item_code
if not frappe.db.exists("Item", bom_item_code):
frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert()
create_items(subtree)
create_items(tree)
def dfs(tree, node):
"""naive implementation for searching right subtree"""
for node_name, subtree in tree.items():
if node_name == node:
return subtree
else:
result = dfs(subtree, node)
if result is not None:
return result
order_of_creating_bom = reversed(level_order_traversal(tree))
for item in order_of_creating_bom:
child_items = dfs(tree, item)
if child_items:
bom_item_code = prefix + item
bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
for child_item in child_items.keys():
bom.append("items", {"item_code": prefix + child_item})
bom.insert()
bom.submit()
return bom # parent bom is last bom
def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None): def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None):
if warehouse_list and isinstance(warehouse_list, string_types): if warehouse_list and isinstance(warehouse_list, str):
warehouse_list = [warehouse_list] warehouse_list = [warehouse_list]
if not warehouse_list: if not warehouse_list:

View File

@ -4,14 +4,24 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cint from frappe.utils import cint, flt
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
class Routing(Document): class Routing(Document):
def validate(self): def validate(self):
self.calculate_operating_cost()
self.set_routing_id() self.set_routing_id()
def on_update(self):
self.calculate_operating_cost()
def calculate_operating_cost(self):
for operation in self.operations:
if not operation.hour_rate:
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
def set_routing_id(self): def set_routing_id(self):
sequence_id = 0 sequence_id = 0
for row in self.operations: for row in self.operations:
@ -21,4 +31,4 @@ class Routing(Document):
frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}") frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}")
.format(row.idx, row.sequence_id, sequence_id)) .format(row.idx, row.sequence_id, sequence_id))
sequence_id = row.sequence_id sequence_id = row.sequence_id

View File

@ -7,9 +7,7 @@ import unittest
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
class TestRouting(unittest.TestCase): class TestRouting(unittest.TestCase):
@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase):
wo_doc.cancel() wo_doc.cancel()
wo_doc.delete() wo_doc.delete()
def test_update_bom_operation_time(self):
operations = [
{
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate_rent": 300,
"hour_rate_labour": 750 ,
"time_in_mins": 30
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation B",
"hour_rate_labour": 200,
"hour_rate_rent": 1000,
"time_in_mins": 20
}
]
test_routing_operations = [
{
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"time_in_mins": 30
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation A",
"time_in_mins": 20
}
]
setup_operations(operations)
routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR')
self.assertEqual(routing_doc.operations[0].time_in_mins, 30)
self.assertEqual(routing_doc.operations[1].time_in_mins, 20)
routing_doc.operations[0].time_in_mins = 90
routing_doc.operations[1].time_in_mins = 42.2
routing_doc.save()
bom_doc.update_cost()
bom_doc.reload()
self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
def setup_operations(rows): def setup_operations(rows):
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
for row in rows: for row in rows:
make_workstation(row) make_workstation(row)
make_operation(row) make_operation(row)
@ -61,12 +105,14 @@ def create_routing(**args):
if not args.do_not_save: if not args.do_not_save:
try: try:
for operation in args.operations:
doc.append("operations", operation)
doc.insert() doc.insert()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
doc = frappe.get_doc("Routing", args.routing_name) doc = frappe.get_doc("Routing", args.routing_name)
doc.delete_key('operations')
for operation in args.operations:
doc.append("operations", operation)
doc.save()
return doc return doc
@ -91,7 +137,7 @@ def setup_bom(**args):
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name') name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
if not name: if not name:
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"), bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
routing = args.routing, with_operations=1) routing = args.routing, with_operations=1, currency = args.currency)
else: else:
bom_doc = frappe.get_doc("BOM", name) bom_doc = frappe.get_doc("BOM", name)

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

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
import json import json
import math import math
@ -28,9 +27,6 @@ class ItemHasVariantError(frappe.ValidationError): pass
from six import string_types from six import string_types
form_grid_templates = {
"operations": "templates/form_grid/work_order_grid.html"
}
class WorkOrder(Document): class WorkOrder(Document):
def onload(self): def onload(self):
@ -393,46 +389,47 @@ class WorkOrder(Document):
def set_work_order_operations(self): def set_work_order_operations(self):
"""Fetch operations from BOM and set in 'Work Order'""" """Fetch operations from BOM and set in 'Work Order'"""
self.set('operations', [])
def _get_operations(bom_no, qty=1):
return frappe.db.sql(
f"""select
operation, description, workstation, idx,
base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
"Pending" as status, parent as bom, batch_size, sequence_id
from
`tabBOM Operation`
where
parent = %s order by idx
""", bom_no, as_dict=1)
self.set('operations', [])
if not self.bom_no: if not self.bom_no:
return return
if self.use_multi_level_bom: operations = []
bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() if not self.use_multi_level_bom:
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
else: else:
bom_list = [self.bom_no] bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
bom_traversal = list(reversed(bom_tree.level_order_traversal()))
bom_traversal.append(bom_tree) # add operation on top level item last
for d in bom_traversal:
if d.is_bom:
operations.extend(_get_operations(d.name, qty=d.exploded_qty))
for correct_index, operation in enumerate(operations, start=1):
operation.idx = correct_index
operations = frappe.db.sql("""
select
operation, description, workstation, idx,
base_hour_rate as hour_rate, time_in_mins,
"Pending" as status, parent as bom, batch_size, sequence_id
from
`tabBOM Operation`
where
parent in (%s) order by idx
""" % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1)
self.set('operations', operations) self.set('operations', operations)
if self.use_multi_level_bom and self.get('operations') and self.get('items'):
raw_material_operations = [d.operation for d in self.get('items')]
operations = [d.operation for d in self.get('operations')]
for operation in raw_material_operations:
if operation not in operations:
self.append('operations', {
'operation': operation
})
self.calculate_time() self.calculate_time()
def calculate_time(self): def calculate_time(self):
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
for d in self.get("operations"): for d in self.get("operations"):
d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
self.calculate_operating_cost() self.calculate_operating_cost()

View File

@ -2,7 +2,6 @@
"actions": [], "actions": [],
"creation": "2014-10-16 14:35:41.950175", "creation": "2014-10-16 14:35:41.950175",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"details", "details",
@ -48,6 +47,7 @@
{ {
"fieldname": "bom", "fieldname": "bom",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "BOM", "label": "BOM",
"no_copy": 1, "no_copy": 1,
"options": "BOM", "options": "BOM",
@ -67,6 +67,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"columns": 1,
"description": "Operation completed for how many finished goods?", "description": "Operation completed for how many finished goods?",
"fieldname": "completed_qty", "fieldname": "completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
@ -76,6 +77,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 1,
"default": "Pending", "default": "Pending",
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
@ -118,6 +120,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"columns": 1,
"description": "in Minutes", "description": "in Minutes",
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Float", "fieldtype": "Float",
@ -200,7 +203,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-10-14 12:58:49.241252", "modified": "2021-06-24 14:36:12.835543",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",
@ -209,4 +212,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
import frappe import frappe
import unittest import unittest
from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError
from erpnext.manufacturing.doctype.routing.test_routing import setup_bom, create_routing
from frappe.test_runner import make_test_records
test_dependencies = ["Warehouse"] test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation') test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation')
class TestWorkstation(unittest.TestCase): class TestWorkstation(unittest.TestCase):
def test_validate_timings(self): def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
@ -21,6 +24,58 @@ class TestWorkstation(unittest.TestCase):
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
def test_update_bom_operation_rate(self):
operations = [
{
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate_rent": 300,
"time_in_mins": 60
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation B",
"hour_rate_rent": 1000,
"time_in_mins": 60
}
]
for row in operations:
make_workstation(row)
make_operation(row)
test_routing_operations = [
{
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"time_in_mins": 60
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation A",
"time_in_mins": 60
}
]
routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations)
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
w1 = frappe.get_doc("Workstation", "_Test Workstation A")
#resets values
w1.hour_rate_rent = 300
w1.hour_rate_labour = 0
w1.save()
bom_doc.update_cost()
bom_doc.reload()
self.assertEqual(w1.hour_rate, 300)
self.assertEqual(bom_doc.operations[0].hour_rate, 300)
w1.hour_rate_rent = 250
w1.save()
#updating after setting new rates in workstations
bom_doc.update_cost()
bom_doc.reload()
self.assertEqual(w1.hour_rate, 250)
self.assertEqual(bom_doc.operations[0].hour_rate, 250)
self.assertEqual(bom_doc.operations[1].hour_rate, 250)
def make_workstation(*args, **kwargs): def make_workstation(*args, **kwargs):
args = args if args else kwargs args = args if args else kwargs
if isinstance(args, tuple): if isinstance(args, tuple):
@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs):
"doctype": "Workstation", "doctype": "Workstation",
"workstation_name": workstation_name "workstation_name": workstation_name
}) })
doc.hour_rate_rent = args.get("hour_rate_rent")
doc.hour_rate_labour = args.get("hour_rate_labour")
doc.insert() doc.insert()
return doc return doc
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
return frappe.get_doc("Workstation", workstation_name) return frappe.get_doc("Workstation", workstation_name)

View File

@ -39,7 +39,8 @@ class Workstation(Document):
def update_bom_operation(self): def update_bom_operation(self):
bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation`
where workstation = %s""", self.name) where workstation = %s and parenttype = 'routing' """, self.name)
for bom_no in bom_list: for bom_no in bom_list:
frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s
where parent = %s and workstation = %s""", where parent = %s and workstation = %s""",
@ -71,7 +72,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da
def is_within_operating_hours(workstation, operation, from_datetime, to_datetime): def is_within_operating_hours(workstation, operation, from_datetime, to_datetime):
operation_length = time_diff_in_seconds(to_datetime, from_datetime) operation_length = time_diff_in_seconds(to_datetime, from_datetime)
workstation = frappe.get_doc("Workstation", workstation) workstation = frappe.get_doc("Workstation", workstation)
if not workstation.working_hours: if not workstation.working_hours:
return return

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

@ -272,11 +272,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
let me = this; let me = this;
let item_codes = []; let item_codes = [];
let item_rates = {}; let item_rates = {};
let item_tax_templates = {};
$.each(this.frm.doc.items || [], function(i, item) { $.each(this.frm.doc.items || [], function(i, item) {
if (item.item_code) { if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times // Use combination of name and item code in case same item is added multiple times
item_codes.push([item.item_code, item.name]); item_codes.push([item.item_code, item.name]);
item_rates[item.name] = item.net_rate; item_rates[item.name] = item.net_rate;
item_tax_templates[item.name] = item.item_tax_template;
} }
}); });
@ -287,18 +290,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
company: me.frm.doc.company, company: me.frm.doc.company,
tax_category: cstr(me.frm.doc.tax_category), tax_category: cstr(me.frm.doc.tax_category),
item_codes: item_codes, item_codes: item_codes,
item_rates: item_rates item_rates: item_rates,
item_tax_templates: item_tax_templates
}, },
callback: function(r) { callback: function(r) {
if (!r.exc) { if (!r.exc) {
$.each(me.frm.doc.items || [], function(i, item) { $.each(me.frm.doc.items || [], function(i, item) {
if (item.name && r.message.hasOwnProperty(item.name)) { if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) {
item.item_tax_template = r.message[item.name].item_tax_template; item.item_tax_template = r.message[item.name].item_tax_template;
item.item_tax_rate = r.message[item.name].item_tax_rate; item.item_tax_rate = r.message[item.name].item_tax_rate;
me.add_taxes_from_item_tax_template(item.item_tax_rate); me.add_taxes_from_item_tax_template(item.item_tax_rate);
} else {
item.item_tax_template = "";
item.item_tax_rate = "{}";
} }
}); });
} }

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

@ -385,13 +385,16 @@ def validate_totals(einvoice):
if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.')) frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1: if abs(
flt(value_details['TotInvVal']) + flt(value_details['Discount']) -
flt(value_details['OthChrg']) - flt(value_details['RndOffAmt']) -
total_item_value) > 1:
frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.')) frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
calculated_invoice_value = \ calculated_invoice_value = \
flt(value_details['AssVal']) + flt(value_details['CgstVal']) \ flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
+ flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \ + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
+ flt(value_details['OthChrg']) - flt(value_details['Discount']) + flt(value_details['OthChrg']) + flt(value_details['RndOffAmt']) - flt(value_details['Discount'])
if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1: if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.')) frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))

View File

@ -201,7 +201,7 @@ class Gstr1Report(object):
elif self.filters.get("type_of_business") == "EXPORT": elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
conditions += " AND billing_address_gstin NOT IN %(company_gstins)s" conditions += " AND IFNULL(billing_address_gstin, '') NOT IN %(company_gstins)s"
return conditions return conditions

View File

@ -233,7 +233,7 @@ class SalesOrder(SellingController):
# Checks Sales Invoice # Checks Sales Invoice
submit_rv = frappe.db.sql_list("""select t1.name submit_rv = frappe.db.sql_list("""select t1.name
from `tabSales Invoice` t1,`tabSales Invoice Item` t2 from `tabSales Invoice` t1,`tabSales Invoice Item` t2
where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""", where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""",
self.name) self.name)
if submit_rv: if submit_rv:

View File

@ -1217,6 +1217,19 @@ class TestSalesOrder(unittest.TestCase):
# To test if the SO does NOT have a Blanket Order # To test if the SO does NOT have a Blanket Order
self.assertEqual(so_doc.items[0].blanket_order, None) self.assertEqual(so_doc.items[0].blanket_order, None)
def test_so_cancellation_when_si_drafted(self):
"""
Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state
Expected result: sales order should not get cancelled
"""
so = make_sales_order()
so.submit()
si = make_sales_invoice(so.name)
si.save()
self.assertRaises(frappe.ValidationError, so.cancel)
def make_sales_order(**args): def make_sales_order(**args):
so = frappe.new_doc("Sales Order") so = frappe.new_doc("Sales Order")

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

@ -407,8 +407,6 @@ def replace_abbr(company, old, new):
frappe.only_for("System Manager") frappe.only_for("System Manager")
frappe.db.set_value("Company", company, "abbr", new)
def _rename_record(doc): def _rename_record(doc):
parts = doc[0].rsplit(" - ", 1) parts = doc[0].rsplit(" - ", 1)
if len(parts) == 1 or parts[1].lower() == old.lower(): if len(parts) == 1 or parts[1].lower() == old.lower():
@ -419,11 +417,18 @@ def replace_abbr(company, old, new):
doc = (d for d in frappe.db.sql("select name from `tab%s` where company=%s" % (dt, '%s'), company)) doc = (d for d in frappe.db.sql("select name from `tab%s` where company=%s" % (dt, '%s'), company))
for d in doc: for d in doc:
_rename_record(d) _rename_record(d)
try:
frappe.db.auto_commit_on_many_writes = 1
frappe.db.set_value("Company", company, "abbr", new)
for dt in ["Warehouse", "Account", "Cost Center", "Department",
"Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
_rename_records(dt)
frappe.db.commit()
for dt in ["Warehouse", "Account", "Cost Center", "Department", except Exception:
"Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: frappe.log_error(title=_('Abbreviation Rename Error'))
_rename_records(dt) finally:
frappe.db.commit() frappe.db.auto_commit_on_many_writes = 0
def get_name_with_abbr(name, company): def get_name_with_abbr(name, company):

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

@ -226,13 +226,12 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
return batch.name return batch.name
def set_batch_nos(doc, warehouse_field, throw=False): def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"):
"""Automatically select `batch_no` for outgoing items in item table""" """Automatically select `batch_no` for outgoing items in item table"""
for d in doc.items: for d in doc.get(child_table):
qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0
has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no')
warehouse = d.get(warehouse_field, None) warehouse = d.get(warehouse_field, None)
if has_batch_no and warehouse and qty > 0: if warehouse and qty > 0 and frappe.db.get_value('Item', d.item_code, 'has_batch_no'):
if not d.batch_no: if not d.batch_no:
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no) d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no)
else: else:
@ -308,4 +307,4 @@ def validate_serial_no_with_batch(serial_nos, item_code):
message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
frappe.throw(_("There is no batch found against the {0}: {1}") frappe.throw(_("There is no batch found against the {0}: {1}")
.format(message, serial_no_link)) .format(message, serial_no_link))

View File

@ -78,6 +78,9 @@ frappe.ui.form.on("Delivery Note", {
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frm.set_df_property('packed_items', 'cannot_add_rows', true);
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
}, },
print_without_amount: function(frm) { print_without_amount: function(frm) {

View File

@ -554,8 +554,7 @@
"oldfieldname": "packing_details", "oldfieldname": "packing_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Packed Item", "options": "Packed Item",
"print_hide": 1, "print_hide": 1
"read_only": 1
}, },
{ {
"fieldname": "product_bundle_help", "fieldname": "product_bundle_help",
@ -1289,7 +1288,7 @@
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-15 23:55:49.620641", "modified": "2021-06-11 19:27:30.901112",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@ -129,12 +129,13 @@ class DeliveryNote(SellingController):
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
self.validate_with_previous_doc() self.validate_with_previous_doc()
if self._action != 'submit' and not self.is_return:
set_batch_nos(self, 'warehouse', True)
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self) make_packing_list(self)
if self._action != 'submit' and not self.is_return:
set_batch_nos(self, 'warehouse', throw=True)
set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items")
self.update_current_stock() self.update_current_stock()
if not self.installation_status: self.installation_status = 'Not Installed' if not self.installation_status: self.installation_status = 'Not Installed'
@ -181,9 +182,8 @@ class DeliveryNote(SellingController):
super(DeliveryNote, self).validate_warehouse() super(DeliveryNote, self).validate_warehouse()
for d in self.get_item_list(): for d in self.get_item_list():
if frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1: if not d['warehouse'] and frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1:
if not d['warehouse']: frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"]))
frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"]))
def update_current_stock(self): def update_current_stock(self):

View File

@ -7,7 +7,7 @@ import unittest
import frappe import frappe
import json import json
import frappe.defaults import frappe.defaults
from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today from frappe.utils import nowdate, nowtime, cstr, flt
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
@ -18,9 +18,11 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWa
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
import create_stock_reconciliation, set_valuation_method import create_stock_reconciliation, set_valuation_method
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
class TestDeliveryNote(unittest.TestCase): class TestDeliveryNote(unittest.TestCase):
def test_over_billing_against_dn(self): def test_over_billing_against_dn(self):
@ -277,8 +279,6 @@ class TestDeliveryNote(unittest.TestCase):
dn.cancel() dn.cancel()
def test_sales_return_for_non_bundled_items_full(self): def test_sales_return_for_non_bundled_items_full(self):
from erpnext.stock.doctype.item.test_item import make_item
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
make_item("Box", {'is_stock_item': 1}) make_item("Box", {'is_stock_item': 1})
@ -741,6 +741,25 @@ class TestDeliveryNote(unittest.TestCase):
self.assertEqual(si2.items[0].qty, 2) self.assertEqual(si2.items[0].qty, 2)
self.assertEqual(si2.items[1].qty, 1) self.assertEqual(si2.items[1].qty, 1)
def test_delivery_note_bundle_with_batched_item(self):
batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0})
batched_item = make_item("_Test Batched Item",
{"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"}
)
make_product_bundle(parent=batched_bundle.name, items=[batched_item.name])
make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42)
try:
dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
except frappe.ValidationError as e:
if "batch" in str(e).lower():
self.fail("Batch numbers not getting added to bundled items in DN.")
raise e
self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item")
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -101,7 +101,8 @@ frappe.ui.form.on('Material Request', {
} }
if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') {
if (flt(frm.doc.per_ordered, 2) < 100) { let precision = frappe.defaults.get_default("float_precision");
if (flt(frm.doc.per_ordered, precision) < 100) {
let add_create_pick_list_button = () => { let add_create_pick_list_button = () => {
frm.add_custom_button(__('Pick List'), frm.add_custom_button(__('Pick List'),
() => frm.events.create_pick_list(frm), __('Create')); () => frm.events.create_pick_list(frm), __('Create'));

View File

@ -189,7 +189,7 @@ class MaterialRequest(BuyingController):
item_wh_list = [] item_wh_list = []
for d in self.get("items"): for d in self.get("items"):
if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \ if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \
and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 and d.warehouse: and d.warehouse and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 :
item_wh_list.append([d.item_code, d.warehouse]) item_wh_list.append([d.item_code, d.warehouse])
for item_code, warehouse in item_wh_list: for item_code, warehouse in item_wh_list:

View File

@ -184,4 +184,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@ -17,6 +17,9 @@ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note a
# TODO: Prioritize SO or WO group warehouse # TODO: Prioritize SO or WO group warehouse
class PickList(Document): class PickList(Document):
def validate(self):
self.validate_for_qty()
def before_save(self): def before_save(self):
self.set_item_locations() self.set_item_locations()
@ -35,6 +38,7 @@ class PickList(Document):
@frappe.whitelist() @frappe.whitelist()
def set_item_locations(self, save=False): def set_item_locations(self, save=False):
self.validate_for_qty()
items = self.aggregate_item_qty() items = self.aggregate_item_qty()
self.item_location_map = frappe._dict() self.item_location_map = frappe._dict()
@ -107,6 +111,11 @@ class PickList(Document):
return item_map.values() return item_map.values()
def validate_for_qty(self):
if self.purpose == "Material Transfer for Manufacture" \
and (self.for_qty is None or self.for_qty == 0):
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
def validate_item_locations(pick_list): def validate_item_locations(pick_list):
if not pick_list.locations: if not pick_list.locations:

View File

@ -37,6 +37,7 @@ class TestPickList(unittest.TestCase):
'company': '_Test Company', 'company': '_Test Company',
'customer': '_Test Customer', 'customer': '_Test Customer',
'items_based_on': 'Sales Order', 'items_based_on': 'Sales Order',
'purpose': 'Delivery',
'locations': [{ 'locations': [{
'item_code': '_Test Item', 'item_code': '_Test Item',
'qty': 5, 'qty': 5,
@ -90,6 +91,7 @@ class TestPickList(unittest.TestCase):
'company': '_Test Company', 'company': '_Test Company',
'customer': '_Test Customer', 'customer': '_Test Customer',
'items_based_on': 'Sales Order', 'items_based_on': 'Sales Order',
'purpose': 'Delivery',
'locations': [{ 'locations': [{
'item_code': '_Test Item Warehouse Group Wise Reorder', 'item_code': '_Test Item Warehouse Group Wise Reorder',
'qty': 1000, 'qty': 1000,
@ -135,6 +137,7 @@ class TestPickList(unittest.TestCase):
'company': '_Test Company', 'company': '_Test Company',
'customer': '_Test Customer', 'customer': '_Test Customer',
'items_based_on': 'Sales Order', 'items_based_on': 'Sales Order',
'purpose': 'Delivery',
'locations': [{ 'locations': [{
'item_code': '_Test Serialized Item', 'item_code': '_Test Serialized Item',
'qty': 1000, 'qty': 1000,
@ -264,6 +267,7 @@ class TestPickList(unittest.TestCase):
'company': '_Test Company', 'company': '_Test Company',
'customer': '_Test Customer', 'customer': '_Test Customer',
'items_based_on': 'Sales Order', 'items_based_on': 'Sales Order',
'purpose': 'Delivery',
'locations': [{ 'locations': [{
'item_code': '_Test Item', 'item_code': '_Test Item',
'qty': 5, 'qty': 5,
@ -319,6 +323,7 @@ class TestPickList(unittest.TestCase):
'company': '_Test Company', 'company': '_Test Company',
'customer': '_Test Customer', 'customer': '_Test Customer',
'items_based_on': 'Sales Order', 'items_based_on': 'Sales Order',
'purpose': 'Delivery',
'locations': [{ 'locations': [{
'item_code': '_Test Item', 'item_code': '_Test Item',
'qty': 1, 'qty': 1,

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()
@ -1004,6 +1011,47 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(pr.status, "To Bill") self.assertEqual(pr.status, "To Bill")
self.assertAlmostEqual(pr.per_billed, 50.0, places=2) self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
def test_service_item_purchase_with_perpetual_inventory(self):
company = '_Test Company with perpetual inventory'
service_item = '_Test Non Stock Item'
before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items')
frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1)
srbnb_account = 'Stock Received But Not Billed - TCP1'
frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account)
pr = make_purchase_receipt(
company=company, item=service_item,
warehouse='Finished Goods - TCP1', do_not_save=1
)
item_row_with_diff_rate = frappe.copy_doc(pr.items[0])
item_row_with_diff_rate.rate = 100
pr.append('items', item_row_with_diff_rate)
pr.save()
pr.submit()
item_one_gl_entry = frappe.db.get_all("GL Entry", {
'voucher_type': pr.doctype,
'voucher_no': pr.name,
'account': srbnb_account,
'voucher_detail_no': pr.items[0].name
}, pluck="name")
item_two_gl_entry = frappe.db.get_all("GL Entry", {
'voucher_type': pr.doctype,
'voucher_no': pr.name,
'account': srbnb_account,
'voucher_detail_no': pr.items[1].name
}, pluck="name")
# check if the entries are not merged into one
# seperate entries should be made since voucher_detail_no is different
self.assertEqual(len(item_one_gl_entry), 1)
self.assertEqual(len(item_two_gl_entry), 1)
frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value)
def get_sl_entries(voucher_type, voucher_no): def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s

View File

@ -436,20 +436,35 @@ def get_barcode_data(items_list):
return itemwise_barcode return itemwise_barcode
@frappe.whitelist() @frappe.whitelist()
def get_item_tax_info(company, tax_category, item_codes, item_rates=None): def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None):
out = {} out = {}
if isinstance(item_codes, string_types):
if item_tax_templates is None:
item_tax_templates = {}
if item_rates is None:
item_rates = {}
if isinstance(item_codes, (str,)):
item_codes = json.loads(item_codes) item_codes = json.loads(item_codes)
if isinstance(item_rates, string_types): if isinstance(item_rates, (str,)):
item_rates = json.loads(item_rates) item_rates = json.loads(item_rates)
if isinstance(item_tax_templates, (str,)):
item_tax_templates = json.loads(item_tax_templates)
for item_code in item_codes: for item_code in item_codes:
if not item_code or item_code[1] in out: if not item_code or item_code[1] in out or not item_tax_templates.get(item_code[1]):
continue continue
out[item_code[1]] = {} out[item_code[1]] = {}
item = frappe.get_cached_doc("Item", item_code[0]) item = frappe.get_cached_doc("Item", item_code[0])
args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])}
if item_tax_templates:
args.update({"item_tax_template": item_tax_templates.get(item_code[1])})
get_item_tax_template(args, item, out[item_code[1]]) get_item_tax_template(args, item, out[item_code[1]])
out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True)
@ -463,9 +478,7 @@ def get_item_tax_template(args, item, out):
} }
""" """
item_tax_template = args.get("item_tax_template") item_tax_template = args.get("item_tax_template")
item_tax_template = _get_item_tax_template(args, item.taxes, out)
if not item_tax_template:
item_tax_template = _get_item_tax_template(args, item.taxes, out)
if not item_tax_template: if not item_tax_template:
item_group = item.item_group item_group = item.item_group
@ -508,7 +521,8 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
return None return None
# do not change if already a valid template # do not change if already a valid template
if args.get('item_tax_template') in taxes: if args.get('item_tax_template') in {t.item_tax_template for t in taxes}:
out["item_tax_template"] = args.get('item_tax_template')
return args.get('item_tax_template') return args.get('item_tax_template')
for tax in taxes: for tax in taxes:

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
}, },
{ {