mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 20:05:09 +00:00
Merge branch 'feat-bom-process-loss' of https://github.com/18alantom/erpnext into feat-bom-process-loss
This commit is contained in:
@@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
|
||||
if address and frappe.db.get_value('Dynamic Link',
|
||||
{'parent': address, 'link_name': company}):
|
||||
filters.append(["Address", "name", "=", address])
|
||||
if not address:
|
||||
filters.append(["Address", "is_shipping_address", "=", 1])
|
||||
|
||||
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
|
||||
|
||||
|
||||
@@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
|
||||
total_days, total_booking_days, account_currency)
|
||||
|
||||
if not amount:
|
||||
return
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
||||
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
||||
|
||||
@@ -19,7 +19,7 @@ class AccountingDimension(Document):
|
||||
|
||||
def validate(self):
|
||||
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
|
||||
'Cost Center', 'Accounting Dimension Detail', 'Company') :
|
||||
'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
|
||||
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
@@ -690,7 +690,7 @@
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.received_amount",
|
||||
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
|
||||
"fieldname": "received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Received Amount After Tax",
|
||||
@@ -707,7 +707,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-09 11:55:04.215050",
|
||||
"modified": "2021-06-22 20:37:06.154206",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -706,7 +706,7 @@ class PaymentEntry(AccountsController):
|
||||
if account_currency != 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"
|
||||
elif self.payment_type == 'Receive':
|
||||
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
|
||||
@@ -761,7 +761,7 @@ class PaymentEntry(AccountsController):
|
||||
return self.advance_tax_account
|
||||
elif self.payment_type == 'Receive':
|
||||
return self.paid_from
|
||||
elif self.payment_type == 'Pay':
|
||||
elif self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
return self.paid_to
|
||||
|
||||
def update_advance_paid(self):
|
||||
|
||||
@@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
});
|
||||
},
|
||||
|
||||
company: function() {
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
},
|
||||
|
||||
onload: function() {
|
||||
this._super();
|
||||
|
||||
@@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
frm: frm,
|
||||
freeze_message: __("Creating Purchase Receipt ...")
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
})
|
||||
|
||||
@@ -966,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
|
||||
|
||||
# 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.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
|
||||
po.save()
|
||||
@@ -1002,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
# Create Purchase Invoice against Purchase Order
|
||||
purchase_invoice = get_mapped_purchase_invoice(po.name)
|
||||
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.save()
|
||||
purchase_invoice.submit()
|
||||
|
||||
@@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
|
||||
def check_if_in_list(gle, gl_map, dimensions=None):
|
||||
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
|
||||
'cost_center', 'project']
|
||||
'cost_center', 'project', 'voucher_detail_no']
|
||||
|
||||
if dimensions:
|
||||
account_head_fieldnames = account_head_fieldnames + dimensions
|
||||
|
||||
@@ -222,7 +222,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
|
||||
if filters.get("account") and not filters.get("include_dimensions"):
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
conditions.append("account in %(account)s")
|
||||
|
||||
|
||||
@@ -168,21 +168,24 @@ def get_columns(filters):
|
||||
"label": _("Income"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 305
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname": "expense",
|
||||
"label": _("Expense"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 305
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname": "gross_profit_loss",
|
||||
"label": _("Gross Profit / Loss"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 307
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
"supp_master_name",
|
||||
"supplier_group",
|
||||
"buying_price_list",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"column_break_3",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"maintain_same_rate",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
@@ -108,6 +109,13 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
|
||||
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bill for Rejected Quantity in Purchase Invoice"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@@ -115,7 +123,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-04 20:01:44.087066",
|
||||
"modified": "2021-06-24 10:38:28.934525",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -828,8 +828,14 @@ class AccountsController(TransactionBase):
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||
|
||||
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
|
||||
def get_company_default(self, fieldname):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
|
||||
@@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabEmployee`
|
||||
where status = 'Active'
|
||||
where status in ('Active', 'Suspended')
|
||||
and docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or employee_name like %(txt)s)
|
||||
|
||||
@@ -99,9 +99,10 @@ def validate_returned_items(doc):
|
||||
frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}")
|
||||
.format(d.idx, s, doc.doctype, doc.return_against))
|
||||
|
||||
if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \
|
||||
and not d.get("warehouse"):
|
||||
frappe.throw(_("Warehouse is mandatory"))
|
||||
if (warehouse_mandatory and not d.get("warehouse") and
|
||||
frappe.db.get_value("Item", d.item_code, "is_stock_item")
|
||||
):
|
||||
frappe.throw(_("Warehouse is mandatory"))
|
||||
|
||||
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):
|
||||
serial_nos.extend(get_serial_nos(row.serial_no))
|
||||
|
||||
return serial_nos
|
||||
return serial_nos
|
||||
|
||||
@@ -330,9 +330,15 @@ class SellingController(StockController):
|
||||
|
||||
# For internal transfers use incoming rate as the valuation rate
|
||||
if self.is_internal_transfer():
|
||||
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
|
||||
if d.rate != rate:
|
||||
d.rate = rate
|
||||
if d.doctype == "Packed Item":
|
||||
incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_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_amount = 0
|
||||
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
|
||||
|
||||
@@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
||||
get_chart_data: function (_columns, result) {
|
||||
return {
|
||||
data: {
|
||||
labels: result.map(d => d[0]),
|
||||
labels: result.map(d => d.creation_date),
|
||||
datasets: [{
|
||||
name: "First Response Time",
|
||||
values: result.map(d => d[1])
|
||||
values: result.map(d => d.first_response_time)
|
||||
}]
|
||||
},
|
||||
type: "line",
|
||||
@@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
||||
hide_days: 0,
|
||||
hide_seconds: 0
|
||||
};
|
||||
value = frappe.utils.get_formatted_duration(d, duration_options);
|
||||
return value;
|
||||
return frappe.utils.get_formatted_duration(d, duration_options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
"label": "Status",
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Active\nInactive\nLeft",
|
||||
"options": "Active\nInactive\nSuspended\nLeft",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -813,7 +813,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-06-12 11:31:37.730760",
|
||||
"modified": "2021-06-17 11:31:37.730760",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr
|
||||
from frappe.utils import getdate, validate_email_address, today, add_years, cstr
|
||||
from frappe.model.naming import set_name_by_naming_series
|
||||
from frappe import throw, _, scrub
|
||||
from frappe.permissions import add_user_permission, remove_user_permission, \
|
||||
@@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
|
||||
from frappe.model.document import Document
|
||||
from erpnext.utilities.transaction_base import delete_events
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
|
||||
|
||||
class EmployeeUserDisabledError(frappe.ValidationError): pass
|
||||
class EmployeeLeftValidationError(frappe.ValidationError): pass
|
||||
@@ -37,7 +36,7 @@ class Employee(NestedSet):
|
||||
|
||||
def validate(self):
|
||||
from erpnext.controllers.status_updater import validate_status
|
||||
validate_status(self.status, ["Active", "Inactive", "Left"])
|
||||
validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
|
||||
|
||||
self.employee = self.name
|
||||
self.set_employee_name()
|
||||
|
||||
@@ -7,7 +7,8 @@ def get_data():
|
||||
'heatmap_message': _('This is based on the attendance of this Employee'),
|
||||
'fieldname': 'employee',
|
||||
'non_standard_fieldnames': {
|
||||
'Bank Account': 'party'
|
||||
'Bank Account': 'party',
|
||||
'Employee Grievance': 'raised_by'
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
@@ -20,7 +21,7 @@ def get_data():
|
||||
},
|
||||
{
|
||||
'label': _('Lifecycle'),
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
|
||||
},
|
||||
{
|
||||
'label': _('Shift'),
|
||||
|
||||
@@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
|
||||
filters: [["status","=", "Active"]],
|
||||
get_indicator: function(doc) {
|
||||
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
||||
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status];
|
||||
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status];
|
||||
return indicator;
|
||||
}
|
||||
};
|
||||
|
||||
0
erpnext/hr/doctype/employee_grievance/__init__.py
Normal file
0
erpnext/hr/doctype/employee_grievance/__init__.py
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Employee Grievance', {
|
||||
setup: function(frm) {
|
||||
frm.set_query('grievance_against_party', function() {
|
||||
return {
|
||||
filters: {
|
||||
name: ['in', [
|
||||
'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee']
|
||||
]
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('associated_document_type', function() {
|
||||
let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website",
|
||||
"Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"];
|
||||
return {
|
||||
filters: {
|
||||
istable: 0,
|
||||
issingle: 0,
|
||||
module: ["Not In", ignore_modules]
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
grievance_against_party: function(frm) {
|
||||
let filters = {};
|
||||
if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) {
|
||||
filters.name = ["!=", frm.doc.raised_by];
|
||||
}
|
||||
frm.set_query('grievance_against', function() {
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal file
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal file
@@ -0,0 +1,261 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "HR-GRIEV-.YYYY.-.#####",
|
||||
"creation": "2021-05-11 13:41:51.485295",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"subject",
|
||||
"raised_by",
|
||||
"employee_name",
|
||||
"designation",
|
||||
"column_break_3",
|
||||
"date",
|
||||
"status",
|
||||
"reports_to",
|
||||
"grievance_details_section",
|
||||
"grievance_against_party",
|
||||
"grievance_against",
|
||||
"grievance_type",
|
||||
"column_break_11",
|
||||
"associated_document_type",
|
||||
"associated_document",
|
||||
"section_break_14",
|
||||
"description",
|
||||
"investigation_details_section",
|
||||
"cause_of_grievance",
|
||||
"resolution_details_section",
|
||||
"resolved_by",
|
||||
"resolution_date",
|
||||
"employee_responsible",
|
||||
"column_break_16",
|
||||
"resolution_detail",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "grievance_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Grievance Type",
|
||||
"options": "Grievance Type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date ",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Open",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Open\nInvestigated\nResolved\nInvalid",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cause_of_grievance",
|
||||
"fieldtype": "Text",
|
||||
"label": "Cause of Grievance",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\""
|
||||
},
|
||||
{
|
||||
"fieldname": "resolution_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Resolution Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "resolved_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Resolved By",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Resolved\"",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee_responsible",
|
||||
"fieldtype": "Link",
|
||||
"label": "Employee Responsible ",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"fieldname": "resolution_detail",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Resolution Details",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "resolution_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Resolution Date",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
|
||||
},
|
||||
{
|
||||
"fieldname": "grievance_against",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Grievance Against",
|
||||
"options": "grievance_against_party",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "raised_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Raised By",
|
||||
"options": "Employee",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Employee Grievance",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "raised_by.designation",
|
||||
"fieldname": "designation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Designation",
|
||||
"options": "Designation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "raised_by.reports_to",
|
||||
"fieldname": "reports_to",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reports To",
|
||||
"options": "Employee",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "grievance_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Grievance Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "grievance_against_party",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Grievance Against Party",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "associated_document_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Associated Document Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "associated_document",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Associated Document",
|
||||
"options": "associated_document_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "investigation_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Investigation Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "raised_by.employee_name",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Employee Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-21 12:51:01.499486",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Grievance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "subject,raised_by,grievance_against_party",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "subject",
|
||||
"track_changes": 1
|
||||
}
|
||||
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal file
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
|
||||
class EmployeeGrievance(Document):
|
||||
def on_submit(self):
|
||||
if self.status not in ["Invalid", "Resolved"]:
|
||||
frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
|
||||
bold("Invalid"),
|
||||
bold("Resolved"))
|
||||
)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
frappe.listview_settings["Employee Grievance"] = {
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function(doc) {
|
||||
var colors = {
|
||||
"Open": "red",
|
||||
"Investigated": "orange",
|
||||
"Resolved": "green",
|
||||
"Invalid": "grey"
|
||||
};
|
||||
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.utils import today
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
class TestEmployeeGrievance(unittest.TestCase):
|
||||
def test_create_employee_grievance(self):
|
||||
create_employee_grievance()
|
||||
|
||||
def create_employee_grievance():
|
||||
grievance_type = create_grievance_type()
|
||||
emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
|
||||
emp_2 = make_employee("testculprit@example.com", company="_Test Company")
|
||||
|
||||
grievance = frappe.new_doc("Employee Grievance")
|
||||
grievance.subject = "Test Employee Grievance"
|
||||
grievance.raised_by = emp_1
|
||||
grievance.date = today()
|
||||
grievance.grievance_type = grievance_type
|
||||
grievance.grievance_against_party = "Employee"
|
||||
grievance.grievance_against = emp_2
|
||||
grievance.description = "test descrip"
|
||||
|
||||
#set cause
|
||||
grievance.cause_of_grievance = "test cause"
|
||||
|
||||
#resolution details
|
||||
grievance.resolution_date = today()
|
||||
grievance.resolution_detail = "test resolution detail"
|
||||
grievance.resolved_by = "test_emp_grievance_@example.com"
|
||||
grievance.employee_responsible = emp_2
|
||||
grievance.status = "Resolved"
|
||||
|
||||
grievance.save()
|
||||
grievance.submit()
|
||||
|
||||
return grievance
|
||||
|
||||
|
||||
def create_grievance_type():
|
||||
if frappe.db.exists("Grievance Type", "Employee Abuse"):
|
||||
return frappe.get_doc("Grievance Type", "Employee Abuse")
|
||||
grievance_type = frappe.new_doc("Grievance Type")
|
||||
grievance_type.name = "Employee Abuse"
|
||||
grievance_type.description = "Test"
|
||||
grievance_type.save()
|
||||
|
||||
return grievance_type.name
|
||||
|
||||
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Grievance Type', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal file
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2021-05-11 12:41:50.256071",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_5",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-21 12:54:37.764712",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Grievance Type",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class GrievanceType(Document):
|
||||
pass
|
||||
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestGrievanceType(unittest.TestCase):
|
||||
pass
|
||||
@@ -2,7 +2,7 @@
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.listview_settings['Job Applicant'] = {
|
||||
add_fields: ["company", "designation", "job_applicant", "status"],
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Accepted") {
|
||||
return [__(doc.status), "green", "status,=," + doc.status];
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
"label": "Allocation"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"bold": 1,
|
||||
"fieldname": "new_leaves_allocated",
|
||||
"fieldtype": "Float",
|
||||
@@ -235,7 +236,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-14 15:28:26.335104",
|
||||
"modified": "2021-06-03 15:28:26.335104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Allocation",
|
||||
@@ -277,4 +278,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"timeline_field": "employee"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from erpnext.hr.utils import set_employee_name, get_leave_period
|
||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
|
||||
|
||||
class OverlapError(frappe.ValidationError): pass
|
||||
class BackDatedAllocationError(frappe.ValidationError): pass
|
||||
@@ -55,6 +56,43 @@ class LeaveAllocation(Document):
|
||||
if self.carry_forward:
|
||||
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if self.has_value_changed("new_leaves_allocated"):
|
||||
self.validate_against_leave_applications()
|
||||
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
|
||||
args = {
|
||||
"leaves": leaves_to_be_added,
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
"is_carry_forward": 0
|
||||
}
|
||||
create_leave_ledger_entry(self, args, True)
|
||||
|
||||
def get_existing_leave_count(self):
|
||||
ledger_entries = frappe.get_all("Leave Ledger Entry",
|
||||
filters={
|
||||
"transaction_type": "Leave Allocation",
|
||||
"transaction_name": self.name,
|
||||
"employee": self.employee,
|
||||
"company": self.company,
|
||||
"leave_type": self.leave_type
|
||||
},
|
||||
pluck="leaves")
|
||||
total_existing_leaves = 0
|
||||
for entry in ledger_entries:
|
||||
total_existing_leaves += entry
|
||||
|
||||
return total_existing_leaves
|
||||
|
||||
def validate_against_leave_applications(self):
|
||||
leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
|
||||
self.from_date, self.to_date)
|
||||
if flt(leaves_taken) > flt(self.total_leaves_allocated):
|
||||
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
|
||||
frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
|
||||
else:
|
||||
frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
|
||||
|
||||
def update_leave_policy_assignments_when_no_allocations_left(self):
|
||||
allocations = frappe.db.get_list("Leave Allocation", filters = {
|
||||
"docstatus": 1,
|
||||
@@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date):
|
||||
|
||||
def validate_carry_forward(leave_type):
|
||||
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
|
||||
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
|
||||
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import erpnext
|
||||
import unittest
|
||||
from frappe.utils import nowdate, add_months, getdate, add_days
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
@@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
leave_allocation.cancel()
|
||||
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
|
||||
|
||||
def test_leave_addition_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
leave_allocation.new_leaves_allocated = 40
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
||||
|
||||
def test_leave_subtraction_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
leave_allocation.new_leaves_allocated = 10
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
||||
|
||||
def test_against_leave_application_validation_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||
leave_application = frappe.get_doc({
|
||||
"doctype": 'Leave Application',
|
||||
"employee": employee.name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"from_date": add_months(nowdate(), 2),
|
||||
"to_date": add_months(add_days(nowdate(), 10), 2),
|
||||
"company": erpnext.get_default_company() or "_Test Company",
|
||||
"docstatus": 1,
|
||||
"status": "Approved",
|
||||
"leave_approver": 'test@example.com'
|
||||
})
|
||||
leave_application.submit()
|
||||
leave_allocation.new_leaves_allocated = 8
|
||||
leave_allocation.total_leaves_allocated = 8
|
||||
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
|
||||
|
||||
def create_leave_allocation(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class StaffingPlan(Document):
|
||||
|
||||
detail.total_estimated_cost = 0
|
||||
if detail.number_of_positions > 0:
|
||||
if detail.vacancies > 0 and detail.estimated_cost_per_position:
|
||||
if detail.vacancies and detail.estimated_cost_per_position:
|
||||
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
|
||||
|
||||
self.total_estimated_budget += detail.total_estimated_cost
|
||||
@@ -76,12 +76,12 @@ class StaffingPlan(Document):
|
||||
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
|
||||
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
|
||||
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
|
||||
for {2} as per staffing plan {3} for parent company {4}."
|
||||
.format(cint(parent_plan_details[0].vacancies),
|
||||
for {2} as per staffing plan {3} for parent company {4}.").format(
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
parent_plan_details[0].total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_plan_details[0].name,
|
||||
parent_company)), ParentCompanyError)
|
||||
parent_company), ParentCompanyError)
|
||||
|
||||
#Get vacanices already planned for all companies down the hierarchy of Parent Company
|
||||
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
|
||||
@@ -98,14 +98,14 @@ class StaffingPlan(Document):
|
||||
(flt(parent_plan_details[0].total_estimated_cost) < \
|
||||
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
|
||||
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
|
||||
.format(cint(all_sibling_details.vacancies),
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
|
||||
cint(all_sibling_details.vacancies),
|
||||
all_sibling_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_company,
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
parent_plan_details[0].total_estimated_cost,
|
||||
parent_plan_details[0].name)))
|
||||
parent_plan_details[0].name))
|
||||
|
||||
def validate_with_subsidiary_plans(self, staffing_plan_detail):
|
||||
#Valdate this plan with all child company plan
|
||||
@@ -121,11 +121,11 @@ class StaffingPlan(Document):
|
||||
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
|
||||
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
|
||||
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
|
||||
.format(self.company,
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
|
||||
self.company,
|
||||
cint(children_details.vacancies),
|
||||
children_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
|
||||
frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_designation_counts(designation, company):
|
||||
@@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
|
||||
designation, from_date, to_date)
|
||||
|
||||
# Only a single staffing plan can be active for a designation on given date
|
||||
return staffing_plan if staffing_plan else None
|
||||
return staffing_plan if staffing_plan else None
|
||||
|
||||
@@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
|
||||
is_carry_forward, is_expired
|
||||
FROM `tabLeave Ledger Entry`
|
||||
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
|
||||
AND docstatus=1 AND leaves>0
|
||||
AND docstatus=1
|
||||
AND (from_date between %(from_date)s AND %(to_date)s
|
||||
OR to_date between %(from_date)s AND %(to_date)s
|
||||
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
|
||||
|
||||
@@ -153,6 +153,24 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Grievance Type",
|
||||
"link_to": "Grievance Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Grievance",
|
||||
"link_to": "Employee Grievance",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
@@ -823,7 +841,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-04-26 13:36:15.413819",
|
||||
"modified": "2021-05-13 17:19:40.524444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR",
|
||||
|
||||
@@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", {
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.toggle_enable("item", frm.doc.__islocal);
|
||||
toggle_operations(frm);
|
||||
|
||||
frm.set_indicator_formatter('item_code',
|
||||
function(doc) {
|
||||
@@ -656,12 +655,6 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) {
|
||||
erpnext.bom.calculate_total(frm.doc);
|
||||
});
|
||||
|
||||
var toggle_operations = function(frm) {
|
||||
frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1);
|
||||
frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1);
|
||||
frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1);
|
||||
};
|
||||
|
||||
frappe.ui.form.on("BOM", "with_operations", function(frm) {
|
||||
if(!cint(frm.doc.with_operations)) {
|
||||
frm.set_value("operations", []);
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
},
|
||||
{
|
||||
"default": "Work Order",
|
||||
"depends_on": "with_operations",
|
||||
"fieldname": "transfer_material_against",
|
||||
"fieldtype": "Select",
|
||||
"label": "Transfer Material Against",
|
||||
@@ -235,6 +236,7 @@
|
||||
{
|
||||
"fieldname": "operations_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
@@ -245,6 +247,7 @@
|
||||
"options": "Routing"
|
||||
},
|
||||
{
|
||||
"depends_on": "with_operations",
|
||||
"fieldname": "operations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Operations",
|
||||
@@ -517,7 +520,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-21 12:29:32.634952",
|
||||
"modified": "2021-03-16 12:25:09.081968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -82,7 +82,8 @@ class BOM(WebsiteGenerator):
|
||||
self.calculate_cost()
|
||||
self.update_stock_qty()
|
||||
self.validate_scrap_items()
|
||||
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):
|
||||
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
||||
@@ -214,7 +215,7 @@ class BOM(WebsiteGenerator):
|
||||
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
|
||||
|
||||
@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:
|
||||
return
|
||||
|
||||
@@ -243,7 +244,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
if self.docstatus == 1:
|
||||
self.flags.ignore_validate_update_after_submit = True
|
||||
self.calculate_cost()
|
||||
self.calculate_cost(update_hour_rate)
|
||||
if save:
|
||||
self.db_update()
|
||||
|
||||
@@ -404,32 +405,47 @@ class BOM(WebsiteGenerator):
|
||||
bom_list.reverse()
|
||||
return bom_list
|
||||
|
||||
def calculate_cost(self):
|
||||
def calculate_cost(self, update_hour_rate = False):
|
||||
"""Calculate bom totals"""
|
||||
self.calculate_op_cost()
|
||||
self.calculate_op_cost(update_hour_rate)
|
||||
self.calculate_rm_cost()
|
||||
self.calculate_sm_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
|
||||
|
||||
def calculate_op_cost(self):
|
||||
def calculate_op_cost(self, update_hour_rate = False):
|
||||
"""Update workstation rate and calculates totals"""
|
||||
self.operating_cost = 0
|
||||
self.base_operating_cost = 0
|
||||
for d in self.get('operations'):
|
||||
if d.workstation:
|
||||
if not d.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.update_rate_and_time(d, update_hour_rate)
|
||||
|
||||
self.operating_cost += flt(d.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):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
total_rm_cost = 0
|
||||
@@ -576,7 +592,7 @@ class BOM(WebsiteGenerator):
|
||||
self.get_routing()
|
||||
|
||||
def validate_operations(self):
|
||||
if self.with_operations and not self.get('operations'):
|
||||
if self.with_operations and not self.get('operations') and self.docstatus == 1:
|
||||
frappe.throw(_("Operations cannot be left blank"))
|
||||
|
||||
if self.with_operations:
|
||||
@@ -1005,7 +1021,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
if filters and filters.get("is_stock_item"):
|
||||
query_filters["is_stock_item"] = 1
|
||||
|
||||
|
||||
return frappe.get_all("Item",
|
||||
fields = fields, filters=query_filters,
|
||||
or_filters = or_cond_filters, order_by=order_by,
|
||||
|
||||
@@ -123,7 +123,7 @@ class TestBOM(unittest.TestCase):
|
||||
bom.items[0].conversion_factor = 5
|
||||
bom.insert()
|
||||
|
||||
bom.update_cost()
|
||||
bom.update_cost(update_hour_rate = False)
|
||||
|
||||
# test amounts in selected currency
|
||||
self.assertEqual(bom.items[0].rate, 300)
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"col_break1",
|
||||
"hour_rate",
|
||||
"time_in_mins",
|
||||
"batch_size",
|
||||
"operating_cost",
|
||||
"base_hour_rate",
|
||||
"base_operating_cost",
|
||||
"batch_size",
|
||||
"image"
|
||||
],
|
||||
"fields": [
|
||||
@@ -61,6 +61,8 @@
|
||||
},
|
||||
{
|
||||
"description": "In minutes",
|
||||
"fetch_from": "operation.total_operation_time",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
@@ -104,7 +106,8 @@
|
||||
"label": "Image"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fetch_from": "operation.batch_size",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "batch_size",
|
||||
"fieldtype": "Int",
|
||||
"label": "Batch Size"
|
||||
@@ -120,7 +123,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-13 18:14:10.018774",
|
||||
"modified": "2021-01-12 14:48:09.596843",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operation",
|
||||
|
||||
@@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_indicator_formatter('sub_operation',
|
||||
function(doc) {
|
||||
if (doc.status == "Pending") {
|
||||
return "red";
|
||||
} else {
|
||||
return doc.status === "Complete" ? "green" : "orange";
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
@@ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', {
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
|
||||
frm.trigger('setup_corrective_job_card');
|
||||
}
|
||||
|
||||
frm.set_query("quality_inspection", function() {
|
||||
return {
|
||||
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
|
||||
@@ -43,12 +57,62 @@ frappe.ui.form.on('Job Card', {
|
||||
|
||||
frm.trigger("toggle_operation_number");
|
||||
|
||||
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
||||
if (frm.doc.docstatus == 0 && !frm.is_new() &&
|
||||
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
||||
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
},
|
||||
|
||||
setup_corrective_job_card: function(frm) {
|
||||
frm.add_custom_button(__('Corrective Job Card'), () => {
|
||||
let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
|
||||
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
|
||||
fieldname: 'operation', get_query() {
|
||||
return {
|
||||
filters: {
|
||||
"is_corrective_operation": 1
|
||||
}
|
||||
};
|
||||
}
|
||||
}, {
|
||||
fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
|
||||
fieldname: 'for_operation', get_query() {
|
||||
return {
|
||||
filters: {
|
||||
"name": ["in", operations]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
frappe.prompt(fields, d => {
|
||||
frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
|
||||
}, __("Select Corrective Operation"));
|
||||
}, __('Make'));
|
||||
},
|
||||
|
||||
make_corrective_job_card: function(frm, operation, for_operation) {
|
||||
frappe.call({
|
||||
method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card',
|
||||
args: {
|
||||
source_name: frm.doc.name,
|
||||
operation: operation,
|
||||
for_operation: for_operation
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
operation: function(frm) {
|
||||
frm.trigger("toggle_operation_number");
|
||||
|
||||
@@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
|
||||
|
||||
prepare_timer_buttons: function(frm) {
|
||||
frm.trigger("make_dashboard");
|
||||
if (!frm.doc.job_started) {
|
||||
frm.add_custom_button(__("Start"), () => {
|
||||
if (!frm.doc.employee) {
|
||||
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
|
||||
fieldname: 'employee'}, d => {
|
||||
if (d.employee) {
|
||||
frm.set_value("employee", d.employee);
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
}
|
||||
}, __("Enter Value"), __("Start"));
|
||||
|
||||
if (!frm.doc.started_time && !frm.doc.current_time) {
|
||||
frm.add_custom_button(__("Start Job"), () => {
|
||||
if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
|
||||
frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
|
||||
options: "Job Card Time Log", fieldname: 'employees'}, d => {
|
||||
frm.events.start_job(frm, "Work In Progress", d.employees);
|
||||
}, __("Assign Job to Employee"));
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
|
||||
}
|
||||
}).addClass("btn-primary");
|
||||
} else if (frm.doc.status == "On Hold") {
|
||||
frm.add_custom_button(__("Resume"), () => {
|
||||
frappe.flags.resume_job = 1;
|
||||
frm.events.start_job(frm);
|
||||
frm.add_custom_button(__("Resume Job"), () => {
|
||||
frm.events.start_job(frm, "Resume Job", frm.doc.employee);
|
||||
}).addClass("btn-primary");
|
||||
} else {
|
||||
frm.add_custom_button(__("Pause"), () => {
|
||||
frappe.flags.pause_job = 1;
|
||||
frm.set_value("status", "On Hold");
|
||||
frm.events.complete_job(frm);
|
||||
frm.add_custom_button(__("Pause Job"), () => {
|
||||
frm.events.complete_job(frm, "On Hold");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Complete"), () => {
|
||||
let completed_time = frappe.datetime.now_datetime();
|
||||
frm.trigger("hide_timer");
|
||||
frm.add_custom_button(__("Complete Job"), () => {
|
||||
var sub_operations = frm.doc.sub_operations;
|
||||
|
||||
if (frm.doc.for_quantity) {
|
||||
let set_qty = true;
|
||||
if (sub_operations && sub_operations.length > 1) {
|
||||
set_qty = false;
|
||||
let last_op_row = sub_operations[sub_operations.length - 2];
|
||||
|
||||
if (last_op_row.status == 'Complete') {
|
||||
set_qty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (set_qty) {
|
||||
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
|
||||
fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => {
|
||||
frm.events.complete_job(frm, completed_time, data.qty);
|
||||
}, __("Enter Value"), __("Complete"));
|
||||
fieldname: 'qty', default: frm.doc.for_quantity}, data => {
|
||||
frm.events.complete_job(frm, "Complete", data.qty);
|
||||
}, __("Enter Value"));
|
||||
} else {
|
||||
frm.events.complete_job(frm, completed_time, 0);
|
||||
frm.events.complete_job(frm, "Complete", 0.0);
|
||||
}
|
||||
}).addClass("btn-primary");
|
||||
}
|
||||
},
|
||||
|
||||
start_job: function(frm) {
|
||||
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
|
||||
row.from_time = frappe.datetime.now_datetime();
|
||||
frm.set_value('job_started', 1);
|
||||
frm.set_value('started_time' , row.from_time);
|
||||
frm.set_value("status", "Work In Progress");
|
||||
|
||||
if (!frappe.flags.resume_job) {
|
||||
frm.set_value('current_time' , 0);
|
||||
}
|
||||
|
||||
frm.save();
|
||||
start_job: function(frm, status, employee) {
|
||||
const args = {
|
||||
job_card_id: frm.doc.name,
|
||||
start_time: frappe.datetime.now_datetime(),
|
||||
employees: employee,
|
||||
status: status
|
||||
};
|
||||
frm.events.make_time_log(frm, args);
|
||||
},
|
||||
|
||||
complete_job: function(frm, completed_time, completed_qty) {
|
||||
frm.doc.time_logs.forEach(d => {
|
||||
if (d.from_time && !d.to_time) {
|
||||
d.to_time = completed_time || frappe.datetime.now_datetime();
|
||||
d.completed_qty = completed_qty || 0;
|
||||
complete_job: function(frm, status, completed_qty) {
|
||||
const args = {
|
||||
job_card_id: frm.doc.name,
|
||||
complete_time: frappe.datetime.now_datetime(),
|
||||
status: status,
|
||||
completed_qty: completed_qty
|
||||
};
|
||||
frm.events.make_time_log(frm, args);
|
||||
},
|
||||
|
||||
if(frappe.flags.pause_job) {
|
||||
let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0;
|
||||
frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0));
|
||||
} else {
|
||||
frm.set_value('started_time' , '');
|
||||
frm.set_value('job_started', 0);
|
||||
frm.set_value('current_time' , 0);
|
||||
}
|
||||
make_time_log: function(frm, args) {
|
||||
frm.events.update_sub_operation(frm, args);
|
||||
|
||||
frm.save();
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
|
||||
args: {
|
||||
args: args
|
||||
},
|
||||
freeze: true,
|
||||
callback: function () {
|
||||
frm.reload_doc();
|
||||
frm.trigger("make_dashboard");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
update_sub_operation: function(frm, args) {
|
||||
if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
|
||||
let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete');
|
||||
if (sub_operations && sub_operations.length) {
|
||||
args["sub_operation"] = sub_operations[0].sub_operation;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
validate: function(frm) {
|
||||
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
|
||||
frm.trigger("reset_timer");
|
||||
}
|
||||
},
|
||||
|
||||
employee: function(frm) {
|
||||
if (frm.doc.job_started && !frm.doc.current_time) {
|
||||
frm.trigger("reset_timer");
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
}
|
||||
},
|
||||
|
||||
reset_timer: function(frm) {
|
||||
frm.set_value('started_time' , '');
|
||||
frm.set_value('job_started', 0);
|
||||
frm.set_value('current_time' , 0);
|
||||
},
|
||||
|
||||
make_dashboard: function(frm) {
|
||||
@@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
|
||||
},
|
||||
|
||||
to_time: function(frm) {
|
||||
frm.set_value('job_started', 0);
|
||||
frm.set_value('started_time', '');
|
||||
}
|
||||
})
|
||||
@@ -9,38 +9,49 @@
|
||||
"naming_series",
|
||||
"work_order",
|
||||
"bom_no",
|
||||
"workstation",
|
||||
"operation",
|
||||
"operation_row_number",
|
||||
"column_break_4",
|
||||
"posting_date",
|
||||
"company",
|
||||
"remarks",
|
||||
"production_section",
|
||||
"production_item",
|
||||
"item_name",
|
||||
"for_quantity",
|
||||
"quality_inspection",
|
||||
"wip_warehouse",
|
||||
"serial_no",
|
||||
"column_break_12",
|
||||
"employee",
|
||||
"employee_name",
|
||||
"status",
|
||||
"wip_warehouse",
|
||||
"quality_inspection",
|
||||
"project",
|
||||
"batch_no",
|
||||
"operation_section_section",
|
||||
"operation",
|
||||
"operation_row_number",
|
||||
"column_break_18",
|
||||
"workstation",
|
||||
"employee",
|
||||
"section_break_21",
|
||||
"sub_operations",
|
||||
"timing_detail",
|
||||
"time_logs",
|
||||
"section_break_13",
|
||||
"total_completed_qty",
|
||||
"total_time_in_mins",
|
||||
"column_break_15",
|
||||
"total_time_in_mins",
|
||||
"section_break_8",
|
||||
"items",
|
||||
"corrective_operation_section",
|
||||
"for_job_card",
|
||||
"is_corrective_job_card",
|
||||
"column_break_33",
|
||||
"hour_rate",
|
||||
"for_operation",
|
||||
"more_information",
|
||||
"operation_id",
|
||||
"sequence_id",
|
||||
"transferred_qty",
|
||||
"requested_qty",
|
||||
"status",
|
||||
"column_break_20",
|
||||
"remarks",
|
||||
"barcode",
|
||||
"job_started",
|
||||
"started_time",
|
||||
@@ -117,13 +128,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Timing Detail"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Employee",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "time_logs",
|
||||
@@ -133,9 +137,11 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "total_completed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Completed Qty",
|
||||
@@ -160,8 +166,7 @@
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Items",
|
||||
"options": "Job Card Item",
|
||||
"read_only": 1
|
||||
"options": "Job Card Item"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -251,12 +256,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.employee_name",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Employee Name"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "production_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Production"
|
||||
@@ -314,11 +314,89 @@
|
||||
"label": "Quality Inspection",
|
||||
"no_copy": 1,
|
||||
"options": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "sub_operations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Operations",
|
||||
"options": "Job Card Operation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operation_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Operation Section"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_21",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_corrective_job_card",
|
||||
"fieldname": "hour_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Hour Rate"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "is_corrective_job_card",
|
||||
"fieldname": "corrective_operation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Corrective Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_corrective_job_card",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Corrective Job Card",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_33",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "for_job_card",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Job Card",
|
||||
"options": "Job Card",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "for_job_card.operation",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "for_operation",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Operation",
|
||||
"options": "Operation"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Employee",
|
||||
"options": "Job Card Time Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"options": "Batch"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-19 18:26:50.531664",
|
||||
"modified": "2021-03-16 15:59:32.766484",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import datetime
|
||||
import json
|
||||
from frappe import _, bold
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
|
||||
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
|
||||
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds)
|
||||
|
||||
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
|
||||
|
||||
@@ -25,10 +26,21 @@ class JobCard(Document):
|
||||
self.set_status()
|
||||
self.validate_operation_id()
|
||||
self.validate_sequence_id()
|
||||
self.get_sub_operations()
|
||||
self.update_sub_operation_status()
|
||||
|
||||
def get_sub_operations(self):
|
||||
if self.operation:
|
||||
self.sub_operations = []
|
||||
for row in frappe.get_all("Sub Operation",
|
||||
filters = {"parent": self.operation}, fields=["operation", "idx"]):
|
||||
row.status = "Pending"
|
||||
row.sub_operation = row.operation
|
||||
self.append("sub_operations", row)
|
||||
|
||||
def validate_time_logs(self):
|
||||
self.total_completed_qty = 0.0
|
||||
self.total_time_in_mins = 0.0
|
||||
self.total_completed_qty = 0.0
|
||||
|
||||
if self.get('time_logs'):
|
||||
for d in self.get('time_logs'):
|
||||
@@ -44,11 +56,14 @@ class JobCard(Document):
|
||||
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
|
||||
self.total_time_in_mins += d.time_in_mins
|
||||
|
||||
if d.completed_qty:
|
||||
if d.completed_qty and not self.sub_operations:
|
||||
self.total_completed_qty += d.completed_qty
|
||||
|
||||
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
|
||||
|
||||
for row in self.sub_operations:
|
||||
self.total_completed_qty += row.completed_qty
|
||||
|
||||
def get_overlap_for(self, args, check_next_available_slot=False):
|
||||
production_capacity = 1
|
||||
|
||||
@@ -57,7 +72,7 @@ class JobCard(Document):
|
||||
self.workstation, 'production_capacity') or 1
|
||||
validate_overlap_for = " and jc.workstation = %(workstation)s "
|
||||
|
||||
if self.employee:
|
||||
if args.get("employee"):
|
||||
# override capacity for employee
|
||||
production_capacity = 1
|
||||
validate_overlap_for = " and jc.employee = %(employee)s "
|
||||
@@ -80,7 +95,7 @@ class JobCard(Document):
|
||||
"to_time": args.to_time,
|
||||
"name": args.name or "No Name",
|
||||
"parent": args.parent or "No Name",
|
||||
"employee": self.employee,
|
||||
"employee": args.get("employee"),
|
||||
"workstation": self.workstation
|
||||
}, as_dict=True)
|
||||
|
||||
@@ -158,6 +173,100 @@ class JobCard(Document):
|
||||
row.planned_start_time = datetime.datetime.combine(start_date,
|
||||
get_time(workstation_doc.working_hours[0].start_time))
|
||||
|
||||
def add_time_log(self, args):
|
||||
last_row = []
|
||||
employees = args.employees
|
||||
if isinstance(employees, str):
|
||||
employees = json.loads(employees)
|
||||
|
||||
if self.time_logs and len(self.time_logs) > 0:
|
||||
last_row = self.time_logs[-1]
|
||||
|
||||
self.reset_timer_value(args)
|
||||
if last_row and args.get("complete_time"):
|
||||
for row in self.time_logs:
|
||||
if not row.to_time:
|
||||
row.update({
|
||||
"to_time": get_datetime(args.get("complete_time")),
|
||||
"operation": args.get("sub_operation"),
|
||||
"completed_qty": args.get("completed_qty") or 0.0
|
||||
})
|
||||
elif args.get("start_time"):
|
||||
for name in employees:
|
||||
self.append("time_logs", {
|
||||
"from_time": get_datetime(args.get("start_time")),
|
||||
"employee": name.get('employee'),
|
||||
"operation": args.get("sub_operation"),
|
||||
"completed_qty": 0.0
|
||||
})
|
||||
|
||||
if not self.employee:
|
||||
self.set_employees(employees)
|
||||
|
||||
if self.status == "On Hold":
|
||||
self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
|
||||
|
||||
self.save()
|
||||
|
||||
def set_employees(self, employees):
|
||||
for name in employees:
|
||||
self.append('employee', {
|
||||
'employee': name.get('employee'),
|
||||
'completed_qty': 0.0
|
||||
})
|
||||
|
||||
def reset_timer_value(self, args):
|
||||
self.started_time = None
|
||||
|
||||
if args.get("status") in ["Work In Progress", "Complete"]:
|
||||
self.current_time = 0.0
|
||||
|
||||
if args.get("status") == "Work In Progress":
|
||||
self.started_time = get_datetime(args.get("start_time"))
|
||||
|
||||
if args.get("status") == "Resume Job":
|
||||
args["status"] = "Work In Progress"
|
||||
|
||||
if args.get("status"):
|
||||
self.status = args.get("status")
|
||||
|
||||
def update_sub_operation_status(self):
|
||||
if not (self.sub_operations and self.time_logs):
|
||||
return
|
||||
|
||||
operation_wise_completed_time = {}
|
||||
for time_log in self.time_logs:
|
||||
if time_log.operation not in operation_wise_completed_time:
|
||||
operation_wise_completed_time.setdefault(time_log.operation,
|
||||
frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
|
||||
|
||||
op_row = operation_wise_completed_time[time_log.operation]
|
||||
op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
|
||||
if self.status == 'On Hold':
|
||||
op_row.status = 'Pause'
|
||||
|
||||
op_row.employee.append(time_log.employee)
|
||||
if time_log.time_in_mins:
|
||||
op_row.completed_time += time_log.time_in_mins
|
||||
op_row.completed_qty += time_log.completed_qty
|
||||
|
||||
for row in self.sub_operations:
|
||||
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
|
||||
if operation_deatils:
|
||||
if row.status != 'Complete':
|
||||
row.status = operation_deatils.status
|
||||
|
||||
row.completed_time = operation_deatils.completed_time
|
||||
if operation_deatils.employee:
|
||||
row.completed_time = row.completed_time / len(set(operation_deatils.employee))
|
||||
|
||||
if operation_deatils.completed_qty:
|
||||
row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
|
||||
else:
|
||||
row.status = 'Pending'
|
||||
row.completed_time = 0.0
|
||||
row.completed_qty = 0.0
|
||||
|
||||
def update_time_logs(self, row):
|
||||
self.append("time_logs", {
|
||||
"from_time": row.planned_start_time,
|
||||
@@ -182,15 +291,18 @@ class JobCard(Document):
|
||||
|
||||
if self.get('operation') == d.operation:
|
||||
self.append('items', {
|
||||
'item_code': d.item_code,
|
||||
'source_warehouse': d.source_warehouse,
|
||||
'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'),
|
||||
'item_name': d.item_name,
|
||||
'description': d.description,
|
||||
'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty
|
||||
"item_code": d.item_code,
|
||||
"source_warehouse": d.source_warehouse,
|
||||
"uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
|
||||
"item_name": d.item_name,
|
||||
"description": d.description,
|
||||
"required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
|
||||
"rate": d.rate,
|
||||
"amount": d.amount
|
||||
})
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_transfer_qty()
|
||||
self.validate_job_card()
|
||||
self.update_work_order()
|
||||
self.set_transferred_qty()
|
||||
@@ -199,7 +311,16 @@ class JobCard(Document):
|
||||
self.update_work_order()
|
||||
self.set_transferred_qty()
|
||||
|
||||
def validate_transfer_qty(self):
|
||||
if self.items and self.transferred_qty < self.for_quantity:
|
||||
frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
|
||||
.format(self.name))
|
||||
|
||||
def validate_job_card(self):
|
||||
if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
|
||||
frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
|
||||
.format(get_link_to_form('Work Order', self.work_order)))
|
||||
|
||||
if not self.time_logs:
|
||||
frappe.throw(_("Time logs are required for {0} {1}")
|
||||
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
|
||||
@@ -215,6 +336,10 @@ class JobCard(Document):
|
||||
if not self.work_order:
|
||||
return
|
||||
|
||||
if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
|
||||
'add_corrective_operation_cost_in_finished_good_valuation')):
|
||||
return
|
||||
|
||||
for_quantity, time_in_mins = 0, 0
|
||||
from_time_list, to_time_list = [], []
|
||||
|
||||
@@ -225,10 +350,24 @@ class JobCard(Document):
|
||||
time_in_mins = flt(data[0].time_in_mins)
|
||||
|
||||
wo = frappe.get_doc('Work Order', self.work_order)
|
||||
if self.operation_id:
|
||||
|
||||
if self.is_corrective_job_card:
|
||||
self.update_corrective_in_work_order(wo)
|
||||
|
||||
elif self.operation_id:
|
||||
self.validate_produced_quantity(for_quantity, wo)
|
||||
self.update_work_order_data(for_quantity, time_in_mins, wo)
|
||||
|
||||
def update_corrective_in_work_order(self, wo):
|
||||
wo.corrective_operation_cost = 0.0
|
||||
for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
|
||||
filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
|
||||
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
|
||||
|
||||
wo.calculate_operating_cost()
|
||||
wo.flags.ignore_validate_update_after_submit = True
|
||||
wo.save()
|
||||
|
||||
def validate_produced_quantity(self, for_quantity, wo):
|
||||
if self.docstatus < 2: return
|
||||
|
||||
@@ -248,8 +387,8 @@ class JobCard(Document):
|
||||
min(from_time) as start_time, max(to_time) as end_time
|
||||
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
|
||||
WHERE
|
||||
jctl.parent = jc.name and jc.work_order = %s
|
||||
and jc.operation_id = %s and jc.docstatus = 1
|
||||
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
|
||||
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
|
||||
""", (self.work_order, self.operation_id), as_dict=1)
|
||||
|
||||
for data in wo.operations:
|
||||
@@ -271,7 +410,8 @@ class JobCard(Document):
|
||||
def get_current_operation_data(self):
|
||||
return frappe.get_all('Job Card',
|
||||
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
|
||||
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
|
||||
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
|
||||
"is_corrective_job_card": 0})
|
||||
|
||||
def set_transferred_qty_in_job_card(self, ste_doc):
|
||||
for row in ste_doc.items:
|
||||
@@ -354,7 +494,11 @@ class JobCard(Document):
|
||||
.format(bold(self.operation), work_order), OperationMismatchError)
|
||||
|
||||
def validate_sequence_id(self):
|
||||
if not (self.work_order and self.sequence_id): return
|
||||
if self.is_corrective_job_card:
|
||||
return
|
||||
|
||||
if not (self.work_order and self.sequence_id):
|
||||
return
|
||||
|
||||
current_operation_qty = 0.0
|
||||
data = self.get_current_operation_data()
|
||||
@@ -376,6 +520,17 @@ class JobCard(Document):
|
||||
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
|
||||
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_time_log(args):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
doc = frappe.get_doc("Job Card", args.job_card_id)
|
||||
doc.validate_sequence_id()
|
||||
doc.add_time_log(args)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_operation_details(work_order, operation):
|
||||
if work_order and operation:
|
||||
@@ -511,3 +666,28 @@ def get_job_details(start, end, filters=None):
|
||||
events.append(job_card_data)
|
||||
|
||||
return events
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
|
||||
def set_missing_values(source, target):
|
||||
target.is_corrective_job_card = 1
|
||||
target.operation = operation
|
||||
target.for_operation = for_operation
|
||||
|
||||
target.set('time_logs', [])
|
||||
target.set('employee', [])
|
||||
target.set('items', [])
|
||||
target.get_sub_operations()
|
||||
target.get_required_items()
|
||||
target.validate_time_logs()
|
||||
|
||||
doclist = get_mapped_doc("Job Card", source_name, {
|
||||
"Job Card": {
|
||||
"doctype": "Job Card",
|
||||
"field_map": {
|
||||
"name": "for_job_card",
|
||||
},
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
return doclist
|
||||
@@ -25,8 +25,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "source_warehouse",
|
||||
@@ -67,8 +66,7 @@
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Required Qty",
|
||||
"read_only": 1
|
||||
"label": "Required Qty"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
@@ -107,7 +105,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-11 13:50:13.804108",
|
||||
"modified": "2021-04-22 18:50:00.003444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Item",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2020-12-07 16:58:38.449041",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sub_operation",
|
||||
"completed_time",
|
||||
"status",
|
||||
"completed_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Complete\nPause\nPending\nWork In Progress",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "In mins",
|
||||
"fieldname": "completed_time",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Completed Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_operation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Operation",
|
||||
"options": "Operation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "completed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Completed Qty",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-16 18:24:35.399593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Operation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class JobCardOperation(Document):
|
||||
pass
|
||||
@@ -1,14 +1,17 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2019-03-08 23:56:43.187569",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"employee",
|
||||
"from_time",
|
||||
"to_time",
|
||||
"column_break_2",
|
||||
"time_in_mins",
|
||||
"completed_qty"
|
||||
"completed_qty",
|
||||
"operation"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -41,10 +44,27 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Completed Qty",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Employee",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"fieldname": "operation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Operation",
|
||||
"no_copy": 1,
|
||||
"options": "Operation",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"modified": "2019-12-03 12:56:02.285448",
|
||||
"links": [],
|
||||
"modified": "2020-12-23 14:30:00.970916",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Time Log",
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
"column_break_16",
|
||||
"overproduction_percentage_for_work_order",
|
||||
"other_settings_section",
|
||||
"update_bom_costs_automatically"
|
||||
"update_bom_costs_automatically",
|
||||
"add_corrective_operation_cost_in_finished_good_valuation",
|
||||
"column_break_23",
|
||||
"make_serial_no_batch_from_work_order"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -155,13 +158,30 @@
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order",
|
||||
"fieldname": "make_serial_no_batch_from_work_order",
|
||||
"fieldtype": "Check",
|
||||
"label": "Make Serial No / Batch from Work Order"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add Corrective Operation Cost in Finished Good Valuation"
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-13 10:55:43.996581",
|
||||
"modified": "2021-03-16 15:54:38.967341",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
@@ -178,4 +198,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,13 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Operation', {
|
||||
refresh: function(frm) {
|
||||
|
||||
setup: function(frm) {
|
||||
frm.set_query('operation', 'sub_operations', function() {
|
||||
return {
|
||||
filters: {
|
||||
'name': ['not in', [frm.doc.name]]
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,167 +1,132 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"beta": 0,
|
||||
"creation": "2014-11-07 16:20:30.683186",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2014-11-07 16:20:30.683186",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"workstation",
|
||||
"data_2",
|
||||
"is_corrective_operation",
|
||||
"job_card_section",
|
||||
"create_job_card_based_on_batch_size",
|
||||
"column_break_6",
|
||||
"batch_size",
|
||||
"sub_operations_section",
|
||||
"sub_operations",
|
||||
"total_operation_time",
|
||||
"section_break_4",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "workstation",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Default Workstation",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Workstation",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "workstation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Default Workstation",
|
||||
"options": "Workstation"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Operation Description"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Description",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "sub_operations_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Sub Operations"
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_operations",
|
||||
"fieldtype": "Table",
|
||||
"options": "Sub Operation"
|
||||
},
|
||||
{
|
||||
"description": "Time in mins.",
|
||||
"fieldname": "total_operation_time",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Operation Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "data_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "create_job_card_based_on_batch_size",
|
||||
"fieldname": "batch_size",
|
||||
"fieldtype": "Int",
|
||||
"label": "Batch Size",
|
||||
"mandatory_depends_on": "create_job_card_based_on_batch_size"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "create_job_card_based_on_batch_size",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Job Card based on Batch Size"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "job_card_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Job Card"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_corrective_operation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Corrective Operation"
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-wrench",
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-11-07 05:28:27.462413",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Operation",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"icon": "fa fa-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-12 15:09:23.593338",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Operation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 0,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 1,
|
||||
"is_custom": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "Manufacturing User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"read": 1,
|
||||
"role": "Manufacturing User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 0,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 1,
|
||||
"is_custom": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -2,9 +2,34 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
class Operation(Document):
|
||||
def validate(self):
|
||||
if not self.description:
|
||||
self.description = self.name
|
||||
|
||||
self.duplicate_sub_operation()
|
||||
self.set_total_time()
|
||||
|
||||
def duplicate_sub_operation(self):
|
||||
operation_list = []
|
||||
for row in self.sub_operations:
|
||||
if row.operation in operation_list:
|
||||
frappe.throw(_("The operation {0} can not add multiple times")
|
||||
.format(frappe.bold(row.operation)))
|
||||
|
||||
if self.name == row.operation:
|
||||
frappe.throw(_("The operation {0} can not be the sub operation")
|
||||
.format(frappe.bold(row.operation)))
|
||||
|
||||
operation_list.append(row.operation)
|
||||
|
||||
def set_total_time(self):
|
||||
self.total_operation_time = 0.0
|
||||
|
||||
for row in self.sub_operations:
|
||||
self.total_operation_time += row.time_in_mins
|
||||
|
||||
@@ -4,14 +4,24 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, flt
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
class Routing(Document):
|
||||
def validate(self):
|
||||
self.calculate_operating_cost()
|
||||
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):
|
||||
sequence_id = 0
|
||||
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}")
|
||||
.format(row.idx, row.sequence_id, sequence_id))
|
||||
|
||||
sequence_id = row.sequence_id
|
||||
sequence_id = row.sequence_id
|
||||
|
||||
@@ -7,9 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
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.workstation.test_workstation import make_workstation
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
|
||||
class TestRouting(unittest.TestCase):
|
||||
@@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase):
|
||||
wo_doc.cancel()
|
||||
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):
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
for row in rows:
|
||||
make_workstation(row)
|
||||
make_operation(row)
|
||||
@@ -61,12 +105,14 @@ def create_routing(**args):
|
||||
|
||||
if not args.do_not_save:
|
||||
try:
|
||||
for operation in args.operations:
|
||||
doc.append("operations", operation)
|
||||
|
||||
doc.insert()
|
||||
except frappe.DuplicateEntryError:
|
||||
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
|
||||
|
||||
@@ -91,7 +137,7 @@ def setup_bom(**args):
|
||||
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
|
||||
if not name:
|
||||
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:
|
||||
bom_doc = frappe.get_doc("BOM", name)
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Sub Operation', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2020-12-07 15:39:47.488519",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"operation",
|
||||
"time_in_mins",
|
||||
"column_break_5",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "operation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Operation",
|
||||
"options": "Operation"
|
||||
},
|
||||
{
|
||||
"description": "Time in mins",
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Operation Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-07 18:09:18.005578",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Sub Operation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
10
erpnext/manufacturing/doctype/sub_operation/sub_operation.py
Normal file
10
erpnext/manufacturing/doctype/sub_operation/sub_operation.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class SubOperation(Document):
|
||||
pass
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestSubOperation(unittest.TestCase):
|
||||
pass
|
||||
@@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", {
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 1
|
||||
&& frm.doc.operations && frm.doc.operations.length
|
||||
&& frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
|
||||
&& frm.doc.operations && frm.doc.operations.length) {
|
||||
|
||||
const not_completed = frm.doc.operations.filter(d => {
|
||||
if(d.status != 'Completed') {
|
||||
@@ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", {
|
||||
const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
|
||||
fields: [
|
||||
{
|
||||
fieldtype:'Link',
|
||||
fieldname:'operation',
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'operation',
|
||||
label: __('Operation'),
|
||||
read_only:1,
|
||||
in_list_view:1
|
||||
read_only: 1,
|
||||
in_list_view: 1
|
||||
},
|
||||
{
|
||||
fieldtype:'Link',
|
||||
fieldname:'workstation',
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'workstation',
|
||||
label: __('Workstation'),
|
||||
read_only:1,
|
||||
in_list_view:1
|
||||
read_only: 1,
|
||||
in_list_view: 1
|
||||
},
|
||||
{
|
||||
fieldtype:'Data',
|
||||
fieldname:'name',
|
||||
fieldtype: 'Data',
|
||||
fieldname: 'name',
|
||||
label: __('Operation Id')
|
||||
},
|
||||
{
|
||||
fieldtype:'Float',
|
||||
fieldname:'pending_qty',
|
||||
fieldtype: 'Float',
|
||||
fieldname: 'pending_qty',
|
||||
label: __('Pending Qty'),
|
||||
},
|
||||
{
|
||||
fieldtype:'Float',
|
||||
fieldname:'qty',
|
||||
fieldtype: 'Float',
|
||||
fieldname: 'qty',
|
||||
label: __('Quantity to Manufacture'),
|
||||
read_only:0,
|
||||
in_list_view:1,
|
||||
read_only: 0,
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Float',
|
||||
fieldname: 'batch_size',
|
||||
label: __('Batch Size'),
|
||||
read_only: 1
|
||||
},
|
||||
],
|
||||
data: operations_data,
|
||||
@@ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", {
|
||||
}, function(data) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
|
||||
freeze: true,
|
||||
args: {
|
||||
work_order: frm.doc.name,
|
||||
operations: data.operations,
|
||||
},
|
||||
callback: function() {
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
}, __("Job Card"), __("Create"));
|
||||
@@ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", {
|
||||
if(data.completed_qty != frm.doc.qty) {
|
||||
pending_qty = frm.doc.qty - flt(data.completed_qty);
|
||||
|
||||
dialog.fields_dict.operations.df.data.push({
|
||||
'name': data.name,
|
||||
'operation': data.operation,
|
||||
'workstation': data.workstation,
|
||||
'qty': pending_qty,
|
||||
'pending_qty': pending_qty,
|
||||
});
|
||||
if (pending_qty) {
|
||||
dialog.fields_dict.operations.df.data.push({
|
||||
'name': data.name,
|
||||
'operation': data.operation,
|
||||
'workstation': data.workstation,
|
||||
'batch_size': data.batch_size,
|
||||
'qty': pending_qty,
|
||||
'pending_qty': pending_qty
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.fields_dict.operations.grid.refresh();
|
||||
@@ -704,6 +716,8 @@ erpnext.work_order = {
|
||||
stop_work_order: function(frm, status) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
|
||||
freeze: true,
|
||||
freeze_message: __("Updating Work Order status"),
|
||||
args: {
|
||||
work_order: frm.doc.name,
|
||||
status: status
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
"produced_qty",
|
||||
"sales_order",
|
||||
"project",
|
||||
"serial_no_and_batch_for_finished_good_section",
|
||||
"has_serial_no",
|
||||
"has_batch_no",
|
||||
"column_break_17",
|
||||
"serial_no",
|
||||
"batch_size",
|
||||
"settings_section",
|
||||
"allow_alternative_item",
|
||||
"use_multi_level_bom",
|
||||
@@ -52,6 +58,7 @@
|
||||
"actual_operating_cost",
|
||||
"additional_operating_cost",
|
||||
"column_break_24",
|
||||
"corrective_operation_cost",
|
||||
"total_operating_cost",
|
||||
"more_info",
|
||||
"description",
|
||||
@@ -488,6 +495,57 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Lead Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "serial_no_and_batch_for_finished_good_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Serial No and Batch for Finished Good"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "production_item.has_serial_no",
|
||||
"fieldname": "has_serial_no",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Serial No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "production_item.has_batch_no",
|
||||
"fieldname": "has_batch_no",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Batch No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "has_serial_no",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Serial Nos",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "has_batch_no",
|
||||
"fieldname": "batch_size",
|
||||
"fieldtype": "Float",
|
||||
"label": "Batch Size"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"description": "From Corrective Job Card",
|
||||
"fieldname": "corrective_operation_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Corrective Operation Cost",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cogs",
|
||||
@@ -495,7 +553,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-16 13:27:51.116484",
|
||||
"modified": "2021-06-20 15:19:14.902699",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -19,14 +19,16 @@ from frappe.utils.csvutils import getlink
|
||||
from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from erpnext.stock.doctype.batch.batch import make_batch
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos
|
||||
|
||||
class OverProductionError(frappe.ValidationError): pass
|
||||
class CapacityError(frappe.ValidationError): pass
|
||||
class StockOverProductionError(frappe.ValidationError): pass
|
||||
class OperationTooLongError(frappe.ValidationError): pass
|
||||
class ItemHasVariantError(frappe.ValidationError): pass
|
||||
|
||||
from six import string_types
|
||||
class SerialNoQtyError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
form_grid_templates = {
|
||||
"operations": "templates/form_grid/work_order_grid.html"
|
||||
@@ -127,7 +129,9 @@ class WorkOrder(Document):
|
||||
|
||||
variable_cost = self.actual_operating_cost if self.actual_operating_cost \
|
||||
else self.planned_operating_cost
|
||||
self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost)
|
||||
|
||||
self.total_operating_cost = (flt(self.additional_operating_cost)
|
||||
+ flt(variable_cost) + flt(self.corrective_operation_cost))
|
||||
|
||||
def validate_work_order_against_so(self):
|
||||
# already ordered qty
|
||||
@@ -235,12 +239,15 @@ class WorkOrder(Document):
|
||||
|
||||
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
|
||||
|
||||
def before_submit(self):
|
||||
self.create_serial_no_batch_no()
|
||||
|
||||
def on_submit(self):
|
||||
if not self.wip_warehouse:
|
||||
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
|
||||
if not self.fg_warehouse:
|
||||
frappe.throw(_("For Warehouse is required before Submit"))
|
||||
|
||||
|
||||
if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}):
|
||||
self.update_work_order_qty_in_combined_so()
|
||||
else:
|
||||
@@ -260,12 +267,76 @@ class WorkOrder(Document):
|
||||
self.update_work_order_qty_in_combined_so()
|
||||
else:
|
||||
self.update_work_order_qty_in_so()
|
||||
|
||||
|
||||
self.delete_job_card()
|
||||
self.update_completed_qty_in_material_request()
|
||||
self.update_planned_qty()
|
||||
self.update_ordered_qty()
|
||||
self.update_reserved_qty_for_production()
|
||||
self.delete_auto_created_batch_and_serial_no()
|
||||
|
||||
def create_serial_no_batch_no(self):
|
||||
if not (self.has_serial_no or self.has_batch_no):
|
||||
return
|
||||
|
||||
if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
|
||||
return
|
||||
|
||||
if self.has_batch_no:
|
||||
self.create_batch_for_finished_good()
|
||||
|
||||
args = {
|
||||
"item_code": self.production_item,
|
||||
"work_order": self.name
|
||||
}
|
||||
|
||||
if self.has_serial_no:
|
||||
self.make_serial_nos(args)
|
||||
|
||||
def create_batch_for_finished_good(self):
|
||||
total_qty = self.qty
|
||||
if not self.batch_size:
|
||||
self.batch_size = total_qty
|
||||
|
||||
while total_qty > 0:
|
||||
qty = self.batch_size
|
||||
if self.batch_size >= total_qty:
|
||||
qty = total_qty
|
||||
|
||||
if total_qty > self.batch_size:
|
||||
total_qty -= self.batch_size
|
||||
else:
|
||||
qty = total_qty
|
||||
total_qty = 0
|
||||
|
||||
make_batch(frappe._dict({
|
||||
"item": self.production_item,
|
||||
"qty_to_produce": qty,
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_name": self.name
|
||||
}))
|
||||
|
||||
def delete_auto_created_batch_and_serial_no(self):
|
||||
for row in frappe.get_all("Serial No", filters = {"work_order": self.name}):
|
||||
frappe.delete_doc("Serial No", row.name)
|
||||
self.db_set("serial_no", "")
|
||||
|
||||
for row in frappe.get_all("Batch", filters = {"reference_name": self.name}):
|
||||
frappe.delete_doc("Batch", row.name)
|
||||
|
||||
def make_serial_nos(self, args):
|
||||
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
|
||||
if serial_no_series:
|
||||
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
|
||||
|
||||
if self.serial_no:
|
||||
args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
|
||||
auto_make_serial_nos(args)
|
||||
|
||||
serial_nos_length = len(get_serial_nos(self.serial_no))
|
||||
if serial_nos_length != self.qty:
|
||||
frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.")
|
||||
.format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError)
|
||||
|
||||
def create_job_card(self):
|
||||
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
|
||||
@@ -273,32 +344,40 @@ class WorkOrder(Document):
|
||||
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
|
||||
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
|
||||
|
||||
for i, row in enumerate(self.operations):
|
||||
self.set_operation_start_end_time(i, row)
|
||||
|
||||
if not row.workstation:
|
||||
frappe.throw(_("Row {0}: select the workstation against the operation {1}")
|
||||
.format(row.idx, row.operation))
|
||||
|
||||
original_start_time = row.planned_start_time
|
||||
job_card_doc = create_job_card(self, row,
|
||||
enable_capacity_planning=enable_capacity_planning, auto_create=True)
|
||||
|
||||
if enable_capacity_planning and job_card_doc:
|
||||
row.planned_start_time = job_card_doc.time_logs[-1].from_time
|
||||
row.planned_end_time = job_card_doc.time_logs[-1].to_time
|
||||
|
||||
if date_diff(row.planned_start_time, original_start_time) > plan_days:
|
||||
frappe.message_log.pop()
|
||||
frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
|
||||
.format(plan_days, row.operation), CapacityError)
|
||||
|
||||
row.db_update()
|
||||
for index, row in enumerate(self.operations):
|
||||
qty = self.qty
|
||||
while qty > 0:
|
||||
qty = split_qty_based_on_batch_size(self, row, qty)
|
||||
if row.job_card_qty > 0:
|
||||
self.prepare_data_for_job_card(row, index,
|
||||
plan_days, enable_capacity_planning)
|
||||
|
||||
planned_end_date = self.operations and self.operations[-1].planned_end_time
|
||||
if planned_end_date:
|
||||
self.db_set("planned_end_date", planned_end_date)
|
||||
|
||||
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
|
||||
self.set_operation_start_end_time(index, row)
|
||||
|
||||
if not row.workstation:
|
||||
frappe.throw(_("Row {0}: select the workstation against the operation {1}")
|
||||
.format(row.idx, row.operation))
|
||||
|
||||
original_start_time = row.planned_start_time
|
||||
job_card_doc = create_job_card(self, row, auto_create=True,
|
||||
enable_capacity_planning=enable_capacity_planning)
|
||||
|
||||
if enable_capacity_planning and job_card_doc:
|
||||
row.planned_start_time = job_card_doc.time_logs[-1].from_time
|
||||
row.planned_end_time = job_card_doc.time_logs[-1].to_time
|
||||
|
||||
if date_diff(row.planned_start_time, original_start_time) > plan_days:
|
||||
frappe.message_log.pop()
|
||||
frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
|
||||
.format(plan_days, row.operation), CapacityError)
|
||||
|
||||
row.db_update()
|
||||
|
||||
def set_operation_start_end_time(self, idx, row):
|
||||
"""Set start and end time for given operation. If first operation, set start as
|
||||
`planned_start_date`, else add time diff to end time of earlier operation."""
|
||||
@@ -365,7 +444,7 @@ class WorkOrder(Document):
|
||||
work_order_qty = qty[0][0] if qty and qty[0][0] else 0
|
||||
frappe.db.set_value('Sales Order Item',
|
||||
self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2))
|
||||
|
||||
|
||||
def update_work_order_qty_in_combined_so(self):
|
||||
total_bundle_qty = 1
|
||||
if self.product_bundle_item:
|
||||
@@ -378,7 +457,7 @@ class WorkOrder(Document):
|
||||
|
||||
prod_plan = frappe.get_doc('Production Plan', self.production_plan)
|
||||
item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item')
|
||||
|
||||
|
||||
for plan_reference in prod_plan.prod_plan_references:
|
||||
work_order_qty = 0.0
|
||||
if plan_reference.item_reference == item_reference:
|
||||
@@ -386,7 +465,7 @@ class WorkOrder(Document):
|
||||
work_order_qty = flt(plan_reference.qty) / total_bundle_qty
|
||||
frappe.db.set_value('Sales Order Item',
|
||||
plan_reference.sales_order_item, 'work_order_qty', work_order_qty)
|
||||
|
||||
|
||||
def update_completed_qty_in_material_request(self):
|
||||
if self.material_request:
|
||||
frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item])
|
||||
@@ -669,6 +748,17 @@ class WorkOrder(Document):
|
||||
bom.set_bom_material_details()
|
||||
return bom
|
||||
|
||||
def update_batch_produced_qty(self, stock_entry_doc):
|
||||
if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
|
||||
return
|
||||
|
||||
for row in stock_entry_doc.items:
|
||||
if row.batch_no and (row.is_finished_item or row.is_scrap_item):
|
||||
qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1},
|
||||
or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0]
|
||||
|
||||
frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
|
||||
@@ -746,7 +836,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None):
|
||||
return wo_doc
|
||||
|
||||
def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
|
||||
if isinstance(variant_items, string_types):
|
||||
if isinstance(variant_items, str):
|
||||
variant_items = json.loads(variant_items)
|
||||
|
||||
for item in variant_items:
|
||||
@@ -826,6 +916,7 @@ def make_stock_entry(work_order_id, purpose, qty=None):
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.get_items()
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
return stock_entry.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -867,13 +958,47 @@ def query_sales_order(production_item):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_job_card(work_order, operations):
|
||||
if isinstance(operations, string_types):
|
||||
if isinstance(operations, str):
|
||||
operations = json.loads(operations)
|
||||
|
||||
work_order = frappe.get_doc('Work Order', work_order)
|
||||
for row in operations:
|
||||
row = frappe._dict(row)
|
||||
validate_operation_data(row)
|
||||
create_job_card(work_order, row, row.get("qty"), auto_create=True)
|
||||
qty = row.get("qty")
|
||||
while qty > 0:
|
||||
qty = split_qty_based_on_batch_size(work_order, row, qty)
|
||||
if row.job_card_qty > 0:
|
||||
create_job_card(work_order, row, auto_create=True)
|
||||
|
||||
def split_qty_based_on_batch_size(wo_doc, row, qty):
|
||||
if not cint(frappe.db.get_value("Operation",
|
||||
row.operation, "create_job_card_based_on_batch_size")):
|
||||
row.batch_size = row.get("qty") or wo_doc.qty
|
||||
|
||||
row.job_card_qty = row.batch_size
|
||||
if row.batch_size and qty >= row.batch_size:
|
||||
qty -= row.batch_size
|
||||
elif qty > 0:
|
||||
row.job_card_qty = qty
|
||||
qty = 0
|
||||
|
||||
get_serial_nos_for_job_card(row, wo_doc)
|
||||
|
||||
return qty
|
||||
|
||||
def get_serial_nos_for_job_card(row, wo_doc):
|
||||
if not wo_doc.serial_no:
|
||||
return
|
||||
|
||||
serial_nos = get_serial_nos(wo_doc.serial_no)
|
||||
used_serial_nos = []
|
||||
for d in frappe.get_all('Job Card', fields=['serial_no'],
|
||||
filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}):
|
||||
used_serial_nos.extend(get_serial_nos(d.serial_no))
|
||||
|
||||
serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos)))
|
||||
row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty])
|
||||
|
||||
def validate_operation_data(row):
|
||||
if row.get("qty") <= 0:
|
||||
@@ -892,20 +1017,22 @@ def validate_operation_data(row):
|
||||
)
|
||||
)
|
||||
|
||||
def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False):
|
||||
def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False):
|
||||
doc = frappe.new_doc("Job Card")
|
||||
doc.update({
|
||||
'work_order': work_order.name,
|
||||
'operation': row.get("operation"),
|
||||
'workstation': row.get("workstation"),
|
||||
'posting_date': nowdate(),
|
||||
'for_quantity': qty or work_order.get('qty', 0),
|
||||
'for_quantity': row.job_card_qty or work_order.get('qty', 0),
|
||||
'operation_id': row.get("name"),
|
||||
'bom_no': work_order.bom_no,
|
||||
'project': work_order.project,
|
||||
'company': work_order.company,
|
||||
'sequence_id': row.get("sequence_id"),
|
||||
'wip_warehouse': work_order.wip_warehouse
|
||||
'wip_warehouse': work_order.wip_warehouse,
|
||||
'hour_rate': row.get("hour_rate"),
|
||||
'serial_no': row.get("serial_no")
|
||||
})
|
||||
|
||||
if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer:
|
||||
|
||||
@@ -4,10 +4,17 @@ from frappe import _
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'work_order',
|
||||
'non_standard_fieldnames': {
|
||||
'Batch': 'reference_name'
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Transactions'),
|
||||
'items': ['Stock Entry', 'Job Card', 'Pick List']
|
||||
},
|
||||
{
|
||||
'label': _('Reference'),
|
||||
'items': ['Serial No', 'Batch']
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,8 +8,9 @@
|
||||
"details",
|
||||
"operation",
|
||||
"bom",
|
||||
"sequence_id",
|
||||
"column_break_4",
|
||||
"description",
|
||||
"sequence_id",
|
||||
"col_break1",
|
||||
"completed_qty",
|
||||
"status",
|
||||
@@ -195,12 +196,16 @@
|
||||
"label": "Sequence ID",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-14 12:58:49.241252",
|
||||
"modified": "2021-01-12 14:48:31.061286",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
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_records = frappe.get_test_records('Workstation')
|
||||
make_test_records('Workstation')
|
||||
|
||||
class TestWorkstation(unittest.TestCase):
|
||||
|
||||
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 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,
|
||||
"_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):
|
||||
args = args if args else kwargs
|
||||
if isinstance(args, tuple):
|
||||
@@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs):
|
||||
"doctype": "Workstation",
|
||||
"workstation_name": workstation_name
|
||||
})
|
||||
|
||||
doc.hour_rate_rent = args.get("hour_rate_rent")
|
||||
doc.hour_rate_labour = args.get("hour_rate_labour")
|
||||
doc.insert()
|
||||
|
||||
return doc
|
||||
except frappe.DuplicateEntryError:
|
||||
return frappe.get_doc("Workstation", workstation_name)
|
||||
return frappe.get_doc("Workstation", workstation_name)
|
||||
|
||||
@@ -39,7 +39,8 @@ class Workstation(Document):
|
||||
|
||||
def update_bom_operation(self):
|
||||
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:
|
||||
frappe.db.sql("""update `tabBOM Operation` set hour_rate = %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):
|
||||
operation_length = time_diff_in_seconds(to_datetime, from_datetime)
|
||||
workstation = frappe.get_doc("Workstation", workstation)
|
||||
|
||||
|
||||
if not workstation.working_hours:
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Cost of Poor Quality Report"] = {
|
||||
"filters": [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("From Date"),
|
||||
fieldname:"from_date",
|
||||
fieldtype: "Datetime",
|
||||
default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("To Date"),
|
||||
fieldname:"to_date",
|
||||
fieldtype: "Datetime",
|
||||
default: frappe.datetime.now_datetime(),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Job Card"),
|
||||
fieldname: "name",
|
||||
fieldtype: "Link",
|
||||
options: "Job Card",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
is_corrective_job_card: 1,
|
||||
docstatus: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Work Order"),
|
||||
fieldname: "work_order",
|
||||
fieldtype: "Link",
|
||||
options: "Work Order"
|
||||
},
|
||||
{
|
||||
label: __("Operation"),
|
||||
fieldname: "operation",
|
||||
fieldtype: "Link",
|
||||
options: "Operation",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
is_corrective_operation: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Workstation"),
|
||||
fieldname: "workstation",
|
||||
fieldtype: "Link",
|
||||
options: "Workstation"
|
||||
},
|
||||
{
|
||||
label: __("Item"),
|
||||
fieldname: "production_item",
|
||||
fieldtype: "Link",
|
||||
options: "Item"
|
||||
},
|
||||
{
|
||||
label: __("Serial No"),
|
||||
fieldname: "serial_no",
|
||||
fieldtype: "Link",
|
||||
options: "Serial No",
|
||||
depends_on: "eval: doc.production_item",
|
||||
get_query: function() {
|
||||
var item_code = frappe.query_report.get_filter_value('production_item');
|
||||
return {
|
||||
filters: {
|
||||
item_code: item_code
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Batch No"),
|
||||
fieldname: "batch_no",
|
||||
fieldtype: "Link",
|
||||
options: "Batch No",
|
||||
depends_on: "eval: doc.production_item",
|
||||
get_query: function() {
|
||||
var item_code = frappe.query_report.get_filter_value('production_item');
|
||||
return {
|
||||
filters: {
|
||||
item: item_code
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-01-11 11:10:58.292896",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"json": "{}",
|
||||
"modified": "2021-01-11 11:11:03.594242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Cost of Poor Quality Report",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Job Card",
|
||||
"report_name": "Cost of Poor Quality Report",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "System Manager"
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing User"
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(report_filters):
|
||||
data = []
|
||||
operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
|
||||
if operations:
|
||||
operations = [d.name for d in operations]
|
||||
fields = ["production_item as item_code", "item_name", "work_order", "operation",
|
||||
"workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
|
||||
|
||||
filters = get_filters(report_filters, operations)
|
||||
|
||||
job_cards = frappe.get_all("Job Card", fields = fields,
|
||||
filters = filters)
|
||||
|
||||
for row in job_cards:
|
||||
row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
|
||||
update_raw_material_cost(row, report_filters)
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
def get_filters(report_filters, operations):
|
||||
filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
|
||||
for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
|
||||
if report_filters.get(field):
|
||||
if field != 'serial_no':
|
||||
filters[field] = report_filters.get(field)
|
||||
else:
|
||||
filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
|
||||
|
||||
return filters
|
||||
|
||||
def update_raw_material_cost(row, filters):
|
||||
row.rm_cost = 0.0
|
||||
for data in frappe.get_all("Job Card Item", fields = ["amount"],
|
||||
filters={"parent": row.name, "docstatus": 1}):
|
||||
row.rm_cost += data.amount
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Job Card"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "name",
|
||||
"options": "Job Card",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Work Order"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "work_order",
|
||||
"options": "Work Order",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Item Code"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "item_code",
|
||||
"options": "Item",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Item Name"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "item_name",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Operation"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "operation",
|
||||
"options": "Operation",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Serial No"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "serial_no",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Batch No"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "batch_no",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Workstation"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "workstation",
|
||||
"options": "Workstation",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Operating Cost"),
|
||||
"fieldtype": "Currency",
|
||||
"fieldname": "operating_cost",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Raw Material Cost"),
|
||||
"fieldtype": "Currency",
|
||||
"fieldname": "rm_cost",
|
||||
"width": "100"
|
||||
},
|
||||
{
|
||||
"label": _("Total Time (in Mins)"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "total_time_in_mins",
|
||||
"width": "100"
|
||||
}
|
||||
]
|
||||
@@ -288,3 +288,5 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
|
||||
erpnext.patches.v13_0.update_timesheet_changes
|
||||
erpnext.patches.v13_0.set_training_event_attendance
|
||||
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
|
||||
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
|
||||
erpnext.patches.v13_0.update_job_card_details
|
||||
|
||||
@@ -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()
|
||||
16
erpnext/patches/v13_0/update_job_card_details.py
Normal file
16
erpnext/patches/v13_0/update_job_card_details.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2019, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("manufacturing", "doctype", "job_card")
|
||||
frappe.reload_doc("manufacturing", "doctype", "job_card_item")
|
||||
frappe.reload_doc("manufacturing", "doctype", "work_order_operation")
|
||||
|
||||
frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo
|
||||
SET jc.hour_rate = wo.hour_rate
|
||||
WHERE
|
||||
jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0
|
||||
""")
|
||||
@@ -12,8 +12,12 @@ frappe.ui.form.on('Additional Salary', {
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
frm.trigger('set_earning_component');
|
||||
onload: function(frm) {
|
||||
if (frm.doc.type) {
|
||||
frm.trigger('set_component_query');
|
||||
}
|
||||
},
|
||||
|
||||
employee: function(frm) {
|
||||
@@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', {
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
frm.trigger('set_earning_component');
|
||||
frm.set_value("type", "");
|
||||
frm.trigger('set_component_query');
|
||||
},
|
||||
|
||||
set_earning_component: function(frm) {
|
||||
set_component_query: function(frm) {
|
||||
if (!frm.doc.company) return;
|
||||
let filters = {company: frm.doc.company};
|
||||
if (frm.doc.type) {
|
||||
filters.type = frm.doc.type;
|
||||
}
|
||||
frm.set_query("salary_component", function() {
|
||||
return {
|
||||
filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company}
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ from frappe import _
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
||||
from frappe.desk.reportview import get_match_cond, get_filters_cond
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||
|
||||
class PayrollEntry(Document):
|
||||
def onload(self):
|
||||
@@ -41,7 +42,7 @@ class PayrollEntry(Document):
|
||||
emp_with_sal_slip.append(employee_details.employee)
|
||||
|
||||
if len(emp_with_sal_slip):
|
||||
frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip)))
|
||||
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||
@@ -211,7 +212,7 @@ class PayrollEntry(Document):
|
||||
return account_dict
|
||||
|
||||
def make_accrual_jv_entry(self):
|
||||
self.check_permission('write')
|
||||
self.check_permission("write")
|
||||
earnings = self.get_salary_component_total(component_type = "earnings") or {}
|
||||
deductions = self.get_salary_component_total(component_type = "deductions") or {}
|
||||
payroll_payable_account = self.payroll_payable_account
|
||||
@@ -219,12 +220,13 @@ class PayrollEntry(Document):
|
||||
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
|
||||
|
||||
if earnings or deductions:
|
||||
journal_entry = frappe.new_doc('Journal Entry')
|
||||
journal_entry.voucher_type = 'Journal Entry'
|
||||
journal_entry.user_remark = _('Accrual Journal Entry for salaries from {0} to {1}')\
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Journal Entry"
|
||||
journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\
|
||||
.format(self.start_date, self.end_date)
|
||||
journal_entry.company = self.company
|
||||
journal_entry.posting_date = self.posting_date
|
||||
accounting_dimensions = get_accounting_dimensions() or []
|
||||
|
||||
accounts = []
|
||||
currencies = []
|
||||
@@ -236,37 +238,34 @@ class PayrollEntry(Document):
|
||||
for acc_cc, amount in earnings.items():
|
||||
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
|
||||
payable_amount += flt(amount, precision)
|
||||
accounts.append({
|
||||
accounts.append(self.update_accounting_dimensions({
|
||||
"account": acc_cc[0],
|
||||
"debit_in_account_currency": flt(amt, precision),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"party_type": '',
|
||||
"cost_center": acc_cc[1] or self.cost_center,
|
||||
"project": self.project
|
||||
})
|
||||
}, accounting_dimensions))
|
||||
|
||||
# Deductions
|
||||
for acc_cc, amount in deductions.items():
|
||||
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
|
||||
payable_amount -= flt(amount, precision)
|
||||
accounts.append({
|
||||
accounts.append(self.update_accounting_dimensions({
|
||||
"account": acc_cc[0],
|
||||
"credit_in_account_currency": flt(amt, precision),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"cost_center": acc_cc[1] or self.cost_center,
|
||||
"party_type": '',
|
||||
"project": self.project
|
||||
})
|
||||
}, accounting_dimensions))
|
||||
|
||||
# Payable amount
|
||||
exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies)
|
||||
accounts.append({
|
||||
accounts.append(self.update_accounting_dimensions({
|
||||
"account": payroll_payable_account,
|
||||
"credit_in_account_currency": flt(payable_amt, precision),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"party_type": '',
|
||||
"cost_center": self.cost_center
|
||||
})
|
||||
}, accounting_dimensions))
|
||||
|
||||
journal_entry.set("accounts", accounts)
|
||||
if len(currencies) > 1:
|
||||
@@ -286,6 +285,12 @@ class PayrollEntry(Document):
|
||||
|
||||
return jv_name
|
||||
|
||||
def update_accounting_dimensions(self, row, accounting_dimensions):
|
||||
for dimension in accounting_dimensions:
|
||||
row.update({dimension: self.get(dimension)})
|
||||
|
||||
return row
|
||||
|
||||
def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies):
|
||||
conversion_rate = 1
|
||||
exchange_rate = self.exchange_rate
|
||||
|
||||
@@ -481,6 +481,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
if not salary_structure:
|
||||
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
|
||||
|
||||
|
||||
employee = frappe.db.get_value("Employee", {"user_id": user})
|
||||
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
|
||||
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
|
||||
|
||||
@@ -124,8 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
|
||||
"doctype": "Salary Structure",
|
||||
"name": salary_structure,
|
||||
"company": company or erpnext.get_default_company(),
|
||||
"earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"payroll_frequency": payroll_frequency,
|
||||
"payment_account": get_random("Account", filters={'account_currency': currency}),
|
||||
"currency": currency
|
||||
|
||||
@@ -43,6 +43,30 @@ class TestProductConfigurator(unittest.TestCase):
|
||||
"show_variant_in_website": 1
|
||||
}).insert()
|
||||
|
||||
def create_regular_web_item(self, name, item_group=None):
|
||||
if not frappe.db.exists('Item', name):
|
||||
doc = frappe.get_doc({
|
||||
"description": name,
|
||||
"item_code": name,
|
||||
"item_name": name,
|
||||
"doctype": "Item",
|
||||
"is_stock_item": 1,
|
||||
"item_group": item_group or "_Test Item Group",
|
||||
"stock_uom": "_Test UOM",
|
||||
"item_defaults": [{
|
||||
"company": "_Test Company",
|
||||
"default_warehouse": "_Test Warehouse - _TC",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"buying_cost_center": "_Test Cost Center - _TC",
|
||||
"selling_cost_center": "_Test Cost Center - _TC",
|
||||
"income_account": "Sales - _TC"
|
||||
}],
|
||||
"show_in_website": 1
|
||||
}).insert()
|
||||
else:
|
||||
doc = frappe.get_doc("Item", name)
|
||||
return doc
|
||||
|
||||
def test_product_list(self):
|
||||
template_items = frappe.get_all('Item', {'show_in_website': 1})
|
||||
variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
|
||||
@@ -79,3 +103,42 @@ class TestProductConfigurator(unittest.TestCase):
|
||||
'Test Size': ['2XL']
|
||||
})
|
||||
self.assertEqual(len(items), 1)
|
||||
|
||||
def test_products_in_multiple_item_groups(self):
|
||||
"""Check if product is visible on multiple item group pages barring its own."""
|
||||
from erpnext.shopping_cart.product_query import ProductQuery
|
||||
|
||||
if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
|
||||
item_group_doc = frappe.get_doc({
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": "Tech Items",
|
||||
"parent_item_group": "All Item Groups",
|
||||
"show_in_website": 1
|
||||
}).insert()
|
||||
else:
|
||||
item_group_doc = frappe.get_doc("Item Group", "Tech Items")
|
||||
|
||||
doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
|
||||
if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
|
||||
doc.append("website_item_groups", {
|
||||
"item_group": "_Test Item Group Desktops"
|
||||
})
|
||||
doc.save()
|
||||
|
||||
# check if item is visible in its own Item Group's page
|
||||
engine = ProductQuery()
|
||||
items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertEqual(items[0].item_code, "Portal Item")
|
||||
|
||||
# check if item is visible in configured foreign Item Group's page
|
||||
engine = ProductQuery()
|
||||
items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
|
||||
item_codes = [row.item_code for row in items]
|
||||
|
||||
self.assertIn(len(items), [2, 3])
|
||||
self.assertIn("Portal Item", item_codes)
|
||||
|
||||
# teardown
|
||||
doc.delete()
|
||||
item_group_doc.delete()
|
||||
@@ -270,11 +270,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
let me = this;
|
||||
let item_codes = [];
|
||||
let item_rates = {};
|
||||
let item_tax_templates = {};
|
||||
|
||||
$.each(this.frm.doc.items || [], function(i, item) {
|
||||
if (item.item_code) {
|
||||
// Use combination of name and item code in case same item is added multiple times
|
||||
item_codes.push([item.item_code, item.name]);
|
||||
item_rates[item.name] = item.net_rate;
|
||||
item_tax_templates[item.name] = item.item_tax_template;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -285,18 +288,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
company: me.frm.doc.company,
|
||||
tax_category: cstr(me.frm.doc.tax_category),
|
||||
item_codes: item_codes,
|
||||
item_rates: item_rates
|
||||
item_rates: item_rates,
|
||||
item_tax_templates: item_tax_templates
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
$.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_rate = r.message[item.name].item_tax_rate;
|
||||
me.add_taxes_from_item_tax_template(item.item_tax_rate);
|
||||
} else {
|
||||
item.item_tax_template = "";
|
||||
item.item_tax_rate = "{}";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -868,9 +868,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
|
||||
}
|
||||
|
||||
if (this.frm.doc.posting_date) var date = this.frm.doc.posting_date;
|
||||
else var date = this.frm.doc.transaction_date;
|
||||
|
||||
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
|
||||
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
|
||||
erpnext.utils.get_shipping_address(this.frm, function(){
|
||||
|
||||
@@ -274,9 +274,9 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
|
||||
return true;
|
||||
}
|
||||
|
||||
erpnext.utils.get_shipping_address = function(frm, callback){
|
||||
erpnext.utils.get_shipping_address = function(frm, callback) {
|
||||
if (frm.doc.company) {
|
||||
if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference ||
|
||||
if ((frm.doc.inter_company_order_reference || frm.doc.internal_invoice_reference ||
|
||||
frm.doc.internal_order_reference)) {
|
||||
if (callback) {
|
||||
return callback();
|
||||
|
||||
@@ -467,11 +467,15 @@ body.product-page {
|
||||
|
||||
.btn-change-address {
|
||||
color: var(--blue-500);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--blue-500);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-new-address:hover, .btn-change-address:hover {
|
||||
box-shadow: none;
|
||||
color: var(--blue-500) !important;
|
||||
border: 1px solid var(--blue-500);
|
||||
}
|
||||
|
||||
.modal .address-card {
|
||||
.card-body {
|
||||
padding: var(--padding-sm);
|
||||
|
||||
@@ -201,7 +201,7 @@ class Gstr1Report(object):
|
||||
elif self.filters.get("type_of_business") == "EXPORT":
|
||||
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
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
from frappe import _
|
||||
|
||||
from frappe.model.document import Document
|
||||
@@ -18,6 +20,27 @@ class ProductBundle(Document):
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
validate_uom_is_integer(self, "uom", "qty")
|
||||
|
||||
def on_trash(self):
|
||||
linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice",
|
||||
"Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"]
|
||||
|
||||
invoice_links = []
|
||||
for doctype in linked_doctypes:
|
||||
item_doctype = doctype + " Item"
|
||||
|
||||
if doctype == "Stock Entry":
|
||||
item_doctype = doctype + " Detail"
|
||||
|
||||
invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"])
|
||||
|
||||
for invoice in invoices:
|
||||
invoice_links.append(get_link_to_form(doctype, invoice['parent']))
|
||||
|
||||
if len(invoice_links):
|
||||
frappe.throw(
|
||||
"This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle"
|
||||
.format(", ".join(invoice_links)), title=_("Not Allowed"))
|
||||
|
||||
def validate_main_item(self):
|
||||
"""Validates, main Item is not a stock item"""
|
||||
if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):
|
||||
|
||||
@@ -241,8 +241,8 @@ erpnext.PointOfSale.Controller = class {
|
||||
events: {
|
||||
get_frm: () => this.frm,
|
||||
|
||||
cart_item_clicked: (item_code, batch_no, uom, rate) => {
|
||||
const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
|
||||
cart_item_clicked: (item) => {
|
||||
const item_row = this.get_item_from_frm(item);
|
||||
this.item_details.toggle_item_details_section(item_row);
|
||||
},
|
||||
|
||||
@@ -273,17 +273,15 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.cart.toggle_numpad(minimize);
|
||||
},
|
||||
|
||||
form_updated: (cdt, cdn, fieldname, value) => {
|
||||
const item_row = frappe.model.get_doc(cdt, cdn);
|
||||
if (item_row && item_row[fieldname] != value) {
|
||||
|
||||
const { item_code, batch_no, uom, rate } = this.item_details.current_item;
|
||||
const event = {
|
||||
field: fieldname,
|
||||
form_updated: (item, field, value) => {
|
||||
const item_row = frappe.model.get_doc(item.doctype, item.name);
|
||||
if (item_row && item_row[field] != value) {
|
||||
const args = {
|
||||
field,
|
||||
value,
|
||||
item: { item_code, batch_no, uom, rate }
|
||||
}
|
||||
return this.on_cart_update(event)
|
||||
item: this.item_details.current_item
|
||||
};
|
||||
return this.on_cart_update(args);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
@@ -300,19 +298,18 @@ erpnext.PointOfSale.Controller = class {
|
||||
set_value_in_current_cart_item: (selector, value) => {
|
||||
this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item);
|
||||
},
|
||||
clone_new_batch_item_in_frm: (batch_serial_map, current_item) => {
|
||||
clone_new_batch_item_in_frm: (batch_serial_map, item) => {
|
||||
// called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches
|
||||
// for each unique batch new item row is added in the form & cart
|
||||
Object.keys(batch_serial_map).forEach(batch => {
|
||||
const { item_code, batch_no } = current_item;
|
||||
const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no);
|
||||
const item_to_clone = this.frm.doc.items.find(i => i.name == item.name);
|
||||
const new_row = this.frm.add_child("items", { ...item_to_clone });
|
||||
// update new serialno and batch
|
||||
new_row.batch_no = batch;
|
||||
new_row.serial_no = batch_serial_map[batch].join(`\n`);
|
||||
new_row.qty = batch_serial_map[batch].length;
|
||||
this.frm.doc.items.forEach(row => {
|
||||
if (item_code === row.item_code) {
|
||||
if (item.item_code === row.item_code) {
|
||||
this.update_cart_html(row);
|
||||
}
|
||||
});
|
||||
@@ -321,8 +318,8 @@ erpnext.PointOfSale.Controller = class {
|
||||
remove_item_from_cart: () => this.remove_item_from_cart(),
|
||||
get_item_stock_map: () => this.item_stock_map,
|
||||
close_item_details: () => {
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
this.cart.prev_action = undefined;
|
||||
this.item_details.toggle_item_details_section(null);
|
||||
this.cart.prev_action = null;
|
||||
this.cart.toggle_item_highlight();
|
||||
},
|
||||
get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse)
|
||||
@@ -506,50 +503,47 @@ erpnext.PointOfSale.Controller = class {
|
||||
let item_row = undefined;
|
||||
try {
|
||||
let { field, value, item } = args;
|
||||
const { item_code, batch_no, serial_no, uom, rate } = item;
|
||||
item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
|
||||
item_row = this.get_item_from_frm(item);
|
||||
const item_row_exists = !$.isEmptyObject(item_row);
|
||||
|
||||
const item_selected_from_selector = field === 'qty' && value === "+1"
|
||||
const from_selector = field === 'qty' && value === "+1";
|
||||
if (from_selector)
|
||||
value = flt(item_row.qty) + flt(value);
|
||||
|
||||
if (item_row) {
|
||||
item_selected_from_selector && (value = item_row.qty + flt(value))
|
||||
|
||||
field === 'qty' && (value = flt(value));
|
||||
if (item_row_exists) {
|
||||
if (field === 'qty')
|
||||
value = flt(value);
|
||||
|
||||
if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) {
|
||||
const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value;
|
||||
await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
|
||||
}
|
||||
|
||||
if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) {
|
||||
if (this.is_current_item_being_edited(item_row) || from_selector) {
|
||||
await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
|
||||
this.update_cart_html(item_row);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (!this.frm.doc.customer) {
|
||||
frappe.dom.unfreeze();
|
||||
frappe.show_alert({
|
||||
message: __('You must select a customer before adding an item.'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
if (!this.frm.doc.customer)
|
||||
return this.raise_customer_selection_alert();
|
||||
|
||||
const { item_code, batch_no, serial_no, rate } = item;
|
||||
|
||||
if (!item_code)
|
||||
return;
|
||||
}
|
||||
if (!item_code) return;
|
||||
|
||||
item_selected_from_selector && (value = flt(value))
|
||||
|
||||
const args = { item_code, batch_no, rate, [field]: value };
|
||||
const new_item = { item_code, batch_no, rate, [field]: value };
|
||||
|
||||
if (serial_no) {
|
||||
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
|
||||
args['serial_no'] = serial_no;
|
||||
new_item['serial_no'] = serial_no;
|
||||
}
|
||||
|
||||
if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0;
|
||||
if (field === 'serial_no')
|
||||
new_item['qty'] = value.split(`\n`).length || 0;
|
||||
|
||||
item_row = this.frm.add_child('items', args);
|
||||
item_row = this.frm.add_child('items', new_item);
|
||||
|
||||
if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
|
||||
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
|
||||
@@ -558,8 +552,11 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
this.update_cart_html(item_row);
|
||||
|
||||
this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row);
|
||||
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
|
||||
if (this.item_details.$component.is(':visible'))
|
||||
this.edit_item_details_of(item_row);
|
||||
|
||||
if (this.check_serial_batch_selection_needed(item_row))
|
||||
this.edit_item_details_of(item_row);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -570,14 +567,33 @@ erpnext.PointOfSale.Controller = class {
|
||||
}
|
||||
}
|
||||
|
||||
get_item_from_frm(item_code, batch_no, uom, rate) {
|
||||
const has_batch_no = batch_no;
|
||||
return this.frm.doc.items.find(
|
||||
i => i.item_code === item_code
|
||||
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
|
||||
&& (i.uom === uom)
|
||||
&& (i.rate == rate)
|
||||
);
|
||||
raise_customer_selection_alert() {
|
||||
frappe.dom.unfreeze();
|
||||
frappe.show_alert({
|
||||
message: __('You must select a customer before adding an item.'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
}
|
||||
|
||||
get_item_from_frm({ name, item_code, batch_no, uom, rate }) {
|
||||
let item_row = null;
|
||||
if (name) {
|
||||
item_row = this.frm.doc.items.find(i => i.name == name);
|
||||
} else {
|
||||
// if item is clicked twice from item selector
|
||||
// then "item_code, batch_no, uom, rate" will help in getting the exact item
|
||||
// to increase the qty by one
|
||||
const has_batch_no = batch_no;
|
||||
item_row = this.frm.doc.items.find(
|
||||
i => i.item_code === item_code
|
||||
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
|
||||
&& (i.uom === uom)
|
||||
&& (i.rate == rate)
|
||||
);
|
||||
}
|
||||
|
||||
return item_row || {};
|
||||
}
|
||||
|
||||
edit_item_details_of(item_row) {
|
||||
@@ -585,9 +601,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
}
|
||||
|
||||
is_current_item_being_edited(item_row) {
|
||||
const { item_code, batch_no } = this.item_details.current_item;
|
||||
|
||||
return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true;
|
||||
return item_row.name == this.item_details.current_item.name;
|
||||
}
|
||||
|
||||
update_cart_html(item_row, remove_item) {
|
||||
@@ -669,7 +683,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
update_item_field(value, field_or_action) {
|
||||
if (field_or_action === 'checkout') {
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
this.item_details.toggle_item_details_section(null);
|
||||
} else if (field_or_action === 'remove') {
|
||||
this.remove_item_from_cart();
|
||||
} else {
|
||||
@@ -688,7 +702,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
.then(() => {
|
||||
frappe.model.clear_doc(doctype, name);
|
||||
this.update_cart_html(current_item, true);
|
||||
this.item_details.toggle_item_details_section(undefined);
|
||||
this.item_details.toggle_item_details_section(null);
|
||||
frappe.dom.unfreeze();
|
||||
})
|
||||
.catch(e => console.log(e));
|
||||
|
||||
@@ -181,11 +181,8 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
me.$totals_section.find(".edit-cart-btn").click();
|
||||
}
|
||||
|
||||
const item_code = unescape($cart_item.attr('data-item-code'));
|
||||
const batch_no = unescape($cart_item.attr('data-batch-no'));
|
||||
const uom = unescape($cart_item.attr('data-uom'));
|
||||
const rate = unescape($cart_item.attr('data-rate'));
|
||||
me.events.cart_item_clicked(item_code, batch_no, uom, rate);
|
||||
const item_row_name = unescape($cart_item.attr('data-row-name'));
|
||||
me.events.cart_item_clicked({ name: item_row_name });
|
||||
this.numpad_value = '';
|
||||
});
|
||||
|
||||
@@ -521,25 +518,14 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
}
|
||||
|
||||
get_cart_item({ item_code, batch_no, uom, rate }) {
|
||||
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
|
||||
const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
|
||||
const uom_attr = `[data-uom="${escape(uom)}"]`;
|
||||
const rate_attr = `[data-rate="${escape(rate)}"]`;
|
||||
|
||||
const item_selector = batch_no ?
|
||||
`.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`;
|
||||
|
||||
get_cart_item({ name }) {
|
||||
const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`;
|
||||
return this.$cart_items_wrapper.find(item_selector);
|
||||
}
|
||||
|
||||
get_item_from_frm(item) {
|
||||
const doc = this.events.get_frm().doc;
|
||||
const { item_code, batch_no, uom, rate } = item;
|
||||
const search_field = batch_no ? 'batch_no' : 'item_code';
|
||||
const search_value = batch_no || item_code;
|
||||
|
||||
return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate);
|
||||
return doc.items.find(i => i.name == item.name);
|
||||
}
|
||||
|
||||
update_item_html(item, remove_item) {
|
||||
@@ -564,10 +550,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
if (!$item_to_update.length) {
|
||||
this.$cart_items_wrapper.append(
|
||||
`<div class="cart-item-wrapper"
|
||||
data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
|
||||
data-batch-no="${escape(item_data.batch_no || '')}" data-rate="${escape(item_data.rate)}">
|
||||
</div>
|
||||
`<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
|
||||
<div class="seperator"></div>`
|
||||
)
|
||||
$item_to_update = this.get_cart_item(item_data);
|
||||
@@ -642,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
function get_item_image_html() {
|
||||
const { image, item_name } = item_data;
|
||||
if (image) {
|
||||
if (!me.hide_images && image) {
|
||||
return `
|
||||
<div class="item-image">
|
||||
<img
|
||||
|
||||
@@ -2,6 +2,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
constructor({ wrapper, events, settings }) {
|
||||
this.wrapper = wrapper;
|
||||
this.events = events;
|
||||
this.hide_images = settings.hide_images;
|
||||
this.allow_rate_change = settings.allow_rate_change;
|
||||
this.allow_discount_change = settings.allow_discount_change;
|
||||
this.current_item = {};
|
||||
@@ -54,36 +55,28 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.$dicount_section = this.$component.find('.discount-section');
|
||||
}
|
||||
|
||||
has_item_has_changed(item) {
|
||||
const { item_code, batch_no, uom, rate } = this.current_item;
|
||||
const item_code_is_same = item && item_code === item.item_code;
|
||||
const batch_is_same = item && batch_no == item.batch_no;
|
||||
const uom_is_same = item && uom === item.uom;
|
||||
const rate_is_same = item && rate === item.rate;
|
||||
|
||||
if (!item)
|
||||
return false;
|
||||
|
||||
if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
compare_with_current_item(item) {
|
||||
// returns true if `item` is currently being edited
|
||||
return item && item.name == this.current_item.name;
|
||||
}
|
||||
|
||||
toggle_item_details_section(item) {
|
||||
this.item_has_changed = this.has_item_has_changed(item);
|
||||
const current_item_changed = !this.compare_with_current_item(item);
|
||||
|
||||
this.events.toggle_item_selector(this.item_has_changed);
|
||||
this.toggle_component(this.item_has_changed);
|
||||
// if item is null or highlighted cart item is clicked twice
|
||||
const hide_item_details = !Boolean(item) || !current_item_changed;
|
||||
|
||||
this.events.toggle_item_selector(!hide_item_details);
|
||||
this.toggle_component(!hide_item_details);
|
||||
|
||||
if (this.item_has_changed) {
|
||||
if (item && current_item_changed) {
|
||||
this.doctype = item.doctype;
|
||||
this.item_meta = frappe.get_meta(this.doctype);
|
||||
this.name = item.name;
|
||||
this.item_row = item;
|
||||
this.currency = this.events.get_frm().doc.currency;
|
||||
|
||||
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate };
|
||||
this.current_item = item;
|
||||
|
||||
this.render_dom(item);
|
||||
this.render_discount_dom(item);
|
||||
@@ -132,7 +125,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.$item_name.html(item_name);
|
||||
this.$item_description.html(get_description_html());
|
||||
this.$item_price.html(format_currency(price_list_rate, this.currency));
|
||||
if (image) {
|
||||
if (!this.hide_images && image) {
|
||||
this.$item_image.html(
|
||||
`<img
|
||||
onerror="cur_pos.item_details.handle_broken_image(this)"
|
||||
@@ -180,7 +173,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
df: {
|
||||
...field_meta,
|
||||
onchange: function() {
|
||||
me.events.form_updated(me.doctype, me.name, fieldname, this.value);
|
||||
me.events.form_updated(me.current_item, fieldname, this.value);
|
||||
}
|
||||
},
|
||||
parent: this.$form_container.find(`.${fieldname}-control`),
|
||||
@@ -218,22 +211,17 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
bind_custom_control_change_event() {
|
||||
const me = this;
|
||||
if (this.rate_control) {
|
||||
if (this.allow_rate_change) {
|
||||
this.rate_control.df.onchange = function() {
|
||||
if (this.value || flt(this.value) === 0) {
|
||||
me.events.set_value_in_current_cart_item('rate', this.value);
|
||||
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
const doc = me.events.get_frm().doc;
|
||||
me.$item_price.html(format_currency(item_row.rate, doc.currency));
|
||||
me.render_discount_dom(item_row);
|
||||
});
|
||||
me.current_item.rate = this.value;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.rate_control.df.read_only = 1;
|
||||
}
|
||||
this.rate_control.df.onchange = function() {
|
||||
if (this.value || flt(this.value) === 0) {
|
||||
me.events.form_updated(me.current_item, 'rate', this.value).then(() => {
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
const doc = me.events.get_frm().doc;
|
||||
me.$item_price.html(format_currency(item_row.rate, doc.currency));
|
||||
me.render_discount_dom(item_row);
|
||||
});
|
||||
}
|
||||
};
|
||||
this.rate_control.df.read_only = !this.allow_rate_change;
|
||||
this.rate_control.refresh();
|
||||
}
|
||||
|
||||
@@ -246,7 +234,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.warehouse_control.df.reqd = 1;
|
||||
this.warehouse_control.df.onchange = function() {
|
||||
if (this.value) {
|
||||
me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => {
|
||||
me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
|
||||
me.item_stock_map = me.events.get_item_stock_map();
|
||||
const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
|
||||
if (available_qty === undefined) {
|
||||
@@ -278,7 +266,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.serial_no_control.df.reqd = 1;
|
||||
this.serial_no_control.df.onchange = async function() {
|
||||
!me.current_item.batch_no && await me.auto_update_batch_no();
|
||||
me.events.form_updated(me.doctype, me.name, 'serial_no', this.value);
|
||||
me.events.form_updated(me.current_item, 'serial_no', this.value);
|
||||
}
|
||||
this.serial_no_control.refresh();
|
||||
}
|
||||
@@ -295,19 +283,12 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.batch_no_control.df.onchange = function() {
|
||||
me.events.set_value_in_current_cart_item('batch-no', this.value);
|
||||
me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
|
||||
me.current_item.batch_no = this.value;
|
||||
}
|
||||
this.batch_no_control.refresh();
|
||||
}
|
||||
|
||||
if (this.uom_control) {
|
||||
this.uom_control.df.onchange = function() {
|
||||
me.events.set_value_in_current_cart_item('uom', this.value);
|
||||
me.events.form_updated(me.doctype, me.name, 'uom', this.value);
|
||||
me.current_item.uom = this.value;
|
||||
me.events.form_updated(me.current_item, 'uom', this.value);
|
||||
|
||||
const item_row = frappe.get_doc(me.doctype, me.name);
|
||||
me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value);
|
||||
@@ -317,9 +298,9 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
|
||||
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
|
||||
const field_control = this[`${fieldname}_control`];
|
||||
const item_is_same = !this.has_item_has_changed(item_row);
|
||||
const item_row_is_being_edited = this.compare_with_current_item(item_row);
|
||||
|
||||
if (item_is_same && field_control && field_control.get_value() !== value) {
|
||||
if (item_row_is_being_edited && field_control && field_control.get_value() !== value) {
|
||||
field_control.set_value(value);
|
||||
cur_pos.update_cart_html(item_row);
|
||||
}
|
||||
@@ -337,7 +318,9 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
fields: ["batch_no", "name"]
|
||||
});
|
||||
const batch_serial_map = serials_with_batch_no.reduce((acc, r) => {
|
||||
acc[r.batch_no] || (acc[r.batch_no] = []);
|
||||
if (!acc[r.batch_no]) {
|
||||
acc[r.batch_no] = [];
|
||||
}
|
||||
acc[r.batch_no] = [...acc[r.batch_no], r.name];
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -353,12 +336,10 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
if (serial_nos_belongs_to_other_batch) {
|
||||
this.serial_no_control.set_value(batch_serial_nos);
|
||||
this.qty_control.set_value(batch_serial_map[batch_no].length);
|
||||
}
|
||||
|
||||
delete batch_serial_map[batch_no];
|
||||
|
||||
if (serial_nos_belongs_to_other_batch)
|
||||
delete batch_serial_map[batch_no];
|
||||
this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,7 +232,11 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
uom = uom === "undefined" ? undefined : uom;
|
||||
rate = rate === "undefined" ? undefined : rate;
|
||||
|
||||
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }});
|
||||
me.events.item_selected({
|
||||
field: 'qty',
|
||||
value: "+1",
|
||||
item: { item_code, batch_no, serial_no, uom, rate }
|
||||
});
|
||||
me.set_search_value('');
|
||||
});
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ def get_data(conditions, filters):
|
||||
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||
soi.qty, soi.delivered_qty,
|
||||
(soi.qty - soi.delivered_qty) AS pending_qty,
|
||||
IFNULL(sii.qty, 0) as billed_qty,
|
||||
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
||||
soi.base_amount as amount,
|
||||
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
|
||||
|
||||
@@ -407,8 +407,6 @@ def replace_abbr(company, old, new):
|
||||
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
frappe.db.set_value("Company", company, "abbr", new)
|
||||
|
||||
def _rename_record(doc):
|
||||
parts = doc[0].rsplit(" - ", 1)
|
||||
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))
|
||||
for d in doc:
|
||||
_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",
|
||||
"Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
|
||||
_rename_records(dt)
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.log_error(title=_('Abbreviation Rename Error'))
|
||||
finally:
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
|
||||
|
||||
def get_name_with_abbr(name, company):
|
||||
|
||||
@@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
field_filters['item_group'] = self.name
|
||||
|
||||
engine = ProductQuery()
|
||||
context.items = engine.query(attribute_filters, field_filters, search, start)
|
||||
context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
|
||||
|
||||
filter_engine = ProductFiltersBuilder(self.name)
|
||||
|
||||
|
||||
@@ -22,12 +22,15 @@ class ProductFiltersBuilder:
|
||||
|
||||
filter_data = []
|
||||
for df in fields:
|
||||
filters = {}
|
||||
filters, or_filters = {}, []
|
||||
if df.fieldtype == "Link":
|
||||
if self.item_group:
|
||||
filters['item_group'] = self.item_group
|
||||
or_filters.extend([
|
||||
["item_group", "=", self.item_group],
|
||||
["Website Item Group", "item_group", "=", self.item_group]
|
||||
])
|
||||
|
||||
values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
|
||||
values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname)
|
||||
else:
|
||||
doctype = df.get_link_doctype()
|
||||
|
||||
@@ -44,7 +47,9 @@ class ProductFiltersBuilder:
|
||||
values = [d.name for d in frappe.get_all(doctype, filters)]
|
||||
|
||||
# Remove None
|
||||
values = values.remove(None) if None in values else values
|
||||
if None in values:
|
||||
values.remove(None)
|
||||
|
||||
if values:
|
||||
filter_data.append([df, values])
|
||||
|
||||
@@ -61,14 +66,18 @@ class ProductFiltersBuilder:
|
||||
for attr_doc in attribute_docs:
|
||||
selected_attributes = []
|
||||
for attr in attr_doc.item_attribute_values:
|
||||
or_filters = []
|
||||
filters= [
|
||||
["Item Variant Attribute", "attribute", "=", attr.parent],
|
||||
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
|
||||
]
|
||||
if self.item_group:
|
||||
filters.append(["item_group", "=", self.item_group])
|
||||
or_filters.extend([
|
||||
["item_group", "=", self.item_group],
|
||||
["Website Item Group", "item_group", "=", self.item_group]
|
||||
])
|
||||
|
||||
if frappe.db.get_all("Item", filters, limit=1):
|
||||
if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
|
||||
selected_attributes.append(attr)
|
||||
|
||||
if selected_attributes:
|
||||
|
||||
@@ -22,13 +22,14 @@ class ProductQuery:
|
||||
self.settings = frappe.get_doc("Products Settings")
|
||||
self.cart_settings = frappe.get_doc("Shopping Cart Settings")
|
||||
self.page_length = self.settings.products_per_page or 20
|
||||
self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route']
|
||||
self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants',
|
||||
'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage']
|
||||
self.filters = []
|
||||
self.or_filters = [['show_in_website', '=', 1]]
|
||||
if not self.settings.get('hide_variants'):
|
||||
self.or_filters.append(['show_variant_in_website', '=', 1])
|
||||
|
||||
def query(self, attributes=None, fields=None, search_term=None, start=0):
|
||||
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
|
||||
"""Summary
|
||||
|
||||
Args:
|
||||
@@ -44,6 +45,15 @@ class ProductQuery:
|
||||
if search_term: self.build_search_filters(search_term)
|
||||
|
||||
result = []
|
||||
website_item_groups = []
|
||||
|
||||
# if from item group page consider website item group table
|
||||
if item_group:
|
||||
website_item_groups = frappe.db.get_all(
|
||||
"Item",
|
||||
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
|
||||
filters=[["Website Item Group", "item_group", "=", item_group]]
|
||||
)
|
||||
|
||||
if attributes:
|
||||
all_items = []
|
||||
@@ -61,12 +71,10 @@ class ProductQuery:
|
||||
],
|
||||
or_filters=self.or_filters,
|
||||
start=start,
|
||||
limit=self.page_length,
|
||||
order_by="weightage desc"
|
||||
limit=self.page_length
|
||||
)
|
||||
|
||||
items_dict = {item.name: item for item in items}
|
||||
# TODO: Replace Variants by their parent templates
|
||||
|
||||
all_items.append(set(items_dict.keys()))
|
||||
|
||||
@@ -78,14 +86,22 @@ class ProductQuery:
|
||||
filters=self.filters,
|
||||
or_filters=self.or_filters,
|
||||
start=start,
|
||||
limit=self.page_length,
|
||||
order_by="weightage desc"
|
||||
limit=self.page_length
|
||||
)
|
||||
|
||||
# Combine results having context of website item groups into item results
|
||||
if item_group and website_item_groups:
|
||||
items_list = {row.name for row in result}
|
||||
for row in website_item_groups:
|
||||
if row.wig_parent not in items_list:
|
||||
result.append(row)
|
||||
|
||||
result = sorted(result, key=lambda x: x.get("weightage"), reverse=True)
|
||||
|
||||
for item in result:
|
||||
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
|
||||
if product_info:
|
||||
item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
|
||||
item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
|
||||
|
||||
return result
|
||||
|
||||
@@ -99,7 +115,16 @@ class ProductQuery:
|
||||
if not values:
|
||||
continue
|
||||
|
||||
if isinstance(values, list):
|
||||
# handle multiselect fields in filter addition
|
||||
meta = frappe.get_meta('Item', cached=True)
|
||||
df = meta.get_field(field)
|
||||
if df.fieldtype == 'Table MultiSelect':
|
||||
child_doctype = df.options
|
||||
child_meta = frappe.get_meta(child_doctype, cached=True)
|
||||
fields = child_meta.get("fields")
|
||||
if fields:
|
||||
self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
|
||||
elif isinstance(values, list):
|
||||
# If value is a list use `IN` query
|
||||
self.filters.append([field, 'IN', values])
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "field:batch_id",
|
||||
"creation": "2013-03-05 14:50:38",
|
||||
@@ -25,7 +26,11 @@
|
||||
"reference_doctype",
|
||||
"reference_name",
|
||||
"section_break_7",
|
||||
"description"
|
||||
"description",
|
||||
"manufacturing_section",
|
||||
"qty_to_produce",
|
||||
"column_break_23",
|
||||
"produced_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -160,13 +165,35 @@
|
||||
"label": "Batch UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manufacturing_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Manufacturing"
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_to_produce",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty To Produce",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Produced Qty",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-archive",
|
||||
"idx": 1,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-01-07 11:10:09.149170",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Batch",
|
||||
|
||||
@@ -226,13 +226,12 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
|
||||
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"""
|
||||
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
|
||||
has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no')
|
||||
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:
|
||||
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no)
|
||||
else:
|
||||
@@ -308,4 +307,9 @@ def validate_serial_no_with_batch(serial_nos, item_code):
|
||||
|
||||
message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
|
||||
frappe.throw(_("There is no batch found against the {0}: {1}")
|
||||
.format(message, serial_no_link))
|
||||
.format(message, serial_no_link))
|
||||
|
||||
def make_batch(args):
|
||||
if frappe.db.get_value("Item", args.item, "has_batch_no"):
|
||||
args.doctype = "Batch"
|
||||
frappe.get_doc(args).insert().name
|
||||
|
||||
@@ -78,6 +78,9 @@ frappe.ui.form.on("Delivery Note", {
|
||||
});
|
||||
|
||||
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) {
|
||||
|
||||
@@ -554,8 +554,7 @@
|
||||
"oldfieldname": "packing_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Packed Item",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "product_bundle_help",
|
||||
@@ -1289,7 +1288,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-15 23:55:49.620641",
|
||||
"modified": "2021-06-11 19:27:30.901112",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user