fix: merge conflict
This commit is contained in:
commit
fa006402b8
@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.5.2'
|
||||
__version__ = '13.6.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
@ -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)
|
||||
|
@ -96,7 +96,7 @@ def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_inte
|
||||
grand_total = 0
|
||||
if rate_of_interest:
|
||||
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
|
||||
dunning_amount = flt(interest_amount) + flt(dunning_fee)
|
||||
return {
|
||||
|
@ -121,8 +121,7 @@ class GLEntry(Document):
|
||||
|
||||
def check_pl_account(self):
|
||||
if self.is_opening=='Yes' and \
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
|
||||
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
|
||||
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
|
||||
|
@ -854,7 +854,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-16 19:33:51.099386",
|
||||
"modified": "2021-06-16 19:43:51.099386",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname":"account",
|
||||
"label": __("Account"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "MultiSelectList",
|
||||
"options": "Account",
|
||||
"get_query": function() {
|
||||
var company = frappe.query_report.get_filter_value('company');
|
||||
return {
|
||||
"doctype": "Account",
|
||||
"filters": {
|
||||
"company": company,
|
||||
}
|
||||
}
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Account', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
|
||||
"label": __("Cost Center"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Cost Center', txt);
|
||||
return frappe.db.get_link_options('Cost Center', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -143,7 +141,9 @@ frappe.query_reports["General Ledger"] = {
|
||||
"label": __("Project"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Project', txt);
|
||||
return frappe.db.get_link_options('Project', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -49,8 +49,12 @@ def validate_filters(filters, account_details):
|
||||
if not filters.get("from_date") and not filters.get("to_date"):
|
||||
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
|
||||
|
||||
if filters.get("account") and not account_details.get(filters.account):
|
||||
frappe.throw(_("Account {0} does not exists").format(filters.account))
|
||||
for account in filters.account:
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if filters.get('account'):
|
||||
filters.account = frappe.parse_json(filters.get('account'))
|
||||
|
||||
if (filters.get("account") and filters.get("group_by") == _('Group by Account')
|
||||
and account_details[filters.account].is_group == 0):
|
||||
@ -87,7 +91,19 @@ def set_account_currency(filters):
|
||||
account_currency = None
|
||||
|
||||
if filters.get("account"):
|
||||
account_currency = get_account_currency(filters.account)
|
||||
if len(filters.get("account")) == 1:
|
||||
account_currency = get_account_currency(filters.account[0])
|
||||
else:
|
||||
currency = get_account_currency(filters.account[0])
|
||||
is_same_account_currency = True
|
||||
for account in filters.get("account"):
|
||||
if get_account_currency(account) != currency:
|
||||
is_same_account_currency = False
|
||||
break
|
||||
|
||||
if is_same_account_currency:
|
||||
account_currency = currency
|
||||
|
||||
elif filters.get("party"):
|
||||
gle_currency = frappe.db.get_value(
|
||||
"GL Entry", {
|
||||
@ -205,10 +221,10 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
if filters.get("account") and not filters.get("include_dimensions"):
|
||||
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
|
||||
conditions.append("""account in (select name from tabAccount
|
||||
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
conditions.append("account in %(account)s")
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
@ -266,6 +282,20 @@ def get_conditions(filters):
|
||||
|
||||
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
def get_accounts_with_children(accounts):
|
||||
if not isinstance(accounts, list):
|
||||
accounts = [d.strip() for d in accounts.strip().split(',') if d]
|
||||
|
||||
all_accounts = []
|
||||
for d in accounts:
|
||||
if frappe.db.exists("Account", d):
|
||||
lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"])
|
||||
children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
|
||||
all_accounts += [c.name for c in children]
|
||||
else:
|
||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_accounts))
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
data = []
|
||||
|
@ -123,7 +123,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-23 19:40:00.120822",
|
||||
"modified": "2021-06-24 10:38:28.934525",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
72
erpnext/change_log/v13/v13_6_0.md
Normal file
72
erpnext/change_log/v13/v13_6_0.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Version 13.6.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523))
|
||||
- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044))
|
||||
- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184))
|
||||
- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878))
|
||||
- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705))
|
||||
- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030))
|
||||
- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696))
|
||||
- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
|
||||
- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176))
|
||||
- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092))
|
||||
- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978))
|
||||
- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073))
|
||||
- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245))
|
||||
- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230))
|
||||
- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125))
|
||||
- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134))
|
||||
- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196))
|
||||
- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083))
|
||||
- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941))
|
||||
- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945))
|
||||
- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011))
|
||||
- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070))
|
||||
- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071))
|
||||
- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122))
|
||||
- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220))
|
||||
- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003))
|
||||
- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229))
|
||||
- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269))
|
||||
- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045))
|
||||
- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170))
|
||||
- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032))
|
||||
- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095))
|
||||
- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023))
|
||||
- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191))
|
||||
- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188))
|
||||
- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217))
|
||||
- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152))
|
||||
- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108))
|
||||
- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202))
|
||||
- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906))
|
||||
- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894))
|
||||
- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997))
|
||||
- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051))
|
||||
- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043))
|
||||
- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143))
|
||||
- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211))
|
||||
- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126))
|
||||
- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192))
|
||||
- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081))
|
||||
- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187))
|
||||
- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195))
|
||||
- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947))
|
||||
- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951))
|
||||
- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968))
|
||||
- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037))
|
||||
- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198))
|
||||
- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100))
|
||||
- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098))
|
||||
- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062))
|
||||
- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031))
|
||||
- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203))
|
||||
- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185))
|
||||
- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934))
|
||||
- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201))
|
@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
||||
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
@ -497,9 +497,6 @@ class StockController(AccountsController):
|
||||
})
|
||||
if future_sle_exists(args):
|
||||
create_repost_item_valuation_entry(args)
|
||||
elif not is_reposting_pending():
|
||||
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||
self.company, self.doctype, self.name)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
|
@ -110,7 +110,7 @@
|
||||
"label": "Allocation"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 1,
|
||||
"fieldname": "new_leaves_allocated",
|
||||
"fieldtype": "Float",
|
||||
|
@ -164,7 +164,6 @@ 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`")
|
||||
@ -179,7 +178,6 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
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)
|
||||
@ -199,8 +197,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
"doctype": 'Leave Application',
|
||||
"employee": employee.name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"from_date": nowdate(),
|
||||
"to_date": add_days(nowdate(), 10),
|
||||
"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",
|
||||
|
@ -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) {
|
||||
@ -326,8 +325,7 @@ frappe.ui.form.on("BOM", {
|
||||
freeze: true,
|
||||
args: {
|
||||
update_parent: true,
|
||||
from_child_bom:false,
|
||||
save: frm.doc.docstatus === 1 ? true : false
|
||||
from_child_bom:false
|
||||
},
|
||||
callback: function(r) {
|
||||
refresh_field("items");
|
||||
@ -651,15 +649,8 @@ 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", []);
|
||||
}
|
||||
toggle_operations(frm);
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -662,7 +662,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:
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase):
|
||||
ste.submit()
|
||||
stock_entries.append(ste)
|
||||
|
||||
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
|
||||
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc')
|
||||
self.assertEqual(len(job_cards), len(bom.operations))
|
||||
|
||||
for i, job_card in enumerate(job_cards):
|
||||
doc = frappe.get_doc("Job Card", job_card)
|
||||
doc.append("time_logs", {
|
||||
"from_time": add_to_date(None, i),
|
||||
"hours": 1,
|
||||
"to_time": add_to_date(None, i + 1),
|
||||
"completed_qty": doc.for_quantity
|
||||
})
|
||||
doc.time_logs[0].completed_qty = 1
|
||||
doc.submit()
|
||||
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
|
||||
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -18,14 +18,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
|
||||
|
||||
|
||||
class WorkOrder(Document):
|
||||
@ -123,7 +125,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
|
||||
@ -231,12 +235,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:
|
||||
@ -256,12 +263,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")
|
||||
@ -269,32 +340,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."""
|
||||
@ -361,7 +440,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:
|
||||
@ -374,7 +453,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:
|
||||
@ -382,7 +461,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])
|
||||
@ -666,6 +745,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):
|
||||
@ -743,7 +833,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:
|
||||
@ -823,6 +913,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()
|
||||
@ -864,13 +955,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:
|
||||
@ -889,20 +1014,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']
|
||||
}
|
||||
]
|
||||
}
|
@ -7,8 +7,9 @@
|
||||
"details",
|
||||
"operation",
|
||||
"bom",
|
||||
"sequence_id",
|
||||
"column_break_4",
|
||||
"description",
|
||||
"sequence_id",
|
||||
"col_break1",
|
||||
"completed_qty",
|
||||
"status",
|
||||
@ -198,6 +199,10 @@
|
||||
"label": "Sequence ID",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
@ -290,3 +290,5 @@ erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
|
||||
erpnext.patches.v13_0.set_training_event_attendance
|
||||
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
|
||||
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
|
||||
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
|
||||
erpnext.patches.v13_0.update_job_card_details
|
||||
|
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
|
||||
""")
|
@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
});
|
||||
|
||||
frm.set_query('employee', 'employees', () => {
|
||||
if (!frm.doc.company) {
|
||||
frappe.msgprint(__("Please set a Company"));
|
||||
return [];
|
||||
let error_fields = [];
|
||||
let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
|
||||
|
||||
let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]);
|
||||
|
||||
mandatory_fields.forEach(field => {
|
||||
if (!frm.doc[field]) {
|
||||
error_fields.push(frappe.unscrub(field));
|
||||
}
|
||||
});
|
||||
|
||||
if (error_fields && error_fields.length) {
|
||||
message = message + '<br><br><ul><li>' + error_fields.join('</li><li>') + "</ul>";
|
||||
frappe.throw({
|
||||
message: message,
|
||||
indicator: 'red',
|
||||
title: __('Missing Fields')
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query",
|
||||
filters: frm.events.get_employee_filters(frm)
|
||||
@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
|
||||
get_employee_filters: function (frm) {
|
||||
let filters = {};
|
||||
filters['company'] = frm.doc.company;
|
||||
filters['start_date'] = frm.doc.start_date;
|
||||
filters['end_date'] = frm.doc.end_date;
|
||||
filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet;
|
||||
filters['payroll_frequency'] = frm.doc.payroll_frequency;
|
||||
filters['payroll_payable_account'] = frm.doc.payroll_payable_account;
|
||||
filters['currency'] = frm.doc.currency;
|
||||
|
||||
if (frm.doc.department) {
|
||||
filters['department'] = frm.doc.department;
|
||||
}
|
||||
if (frm.doc.branch) {
|
||||
filters['branch'] = frm.doc.branch;
|
||||
}
|
||||
if (frm.doc.designation) {
|
||||
filters['designation'] = frm.doc.designation;
|
||||
}
|
||||
let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account',
|
||||
'currency', 'department', 'branch', 'designation'];
|
||||
|
||||
fields.forEach(field => {
|
||||
if (frm.doc[field]) {
|
||||
filters[field] = frm.doc[field];
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.doc.employees) {
|
||||
filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
|
||||
let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
|
||||
if (employees && employees.length) {
|
||||
filters['employees'] = employees;
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
},
|
||||
|
@ -680,6 +680,10 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions = []
|
||||
include_employees = []
|
||||
emp_cond = ''
|
||||
|
||||
if not filters.payroll_frequency:
|
||||
frappe.throw(_('Select Payroll Frequency.'))
|
||||
|
||||
if filters.start_date and filters.end_date:
|
||||
employee_list = get_employee_list(filters)
|
||||
emp = filters.get('employees')
|
||||
|
@ -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"):
|
||||
|
@ -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",
|
||||
|
@ -308,3 +308,8 @@ 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))
|
||||
|
||||
def make_batch(args):
|
||||
if frappe.db.get_value("Item", args.item, "has_batch_no"):
|
||||
args.doctype = "Batch"
|
||||
frappe.get_doc(args).insert().name
|
||||
|
@ -57,7 +57,8 @@
|
||||
"more_info",
|
||||
"serial_no_details",
|
||||
"company",
|
||||
"status"
|
||||
"status",
|
||||
"work_order"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -422,12 +423,18 @@
|
||||
"label": "Status",
|
||||
"options": "\nActive\nInactive\nDelivered\nExpired",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "work_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Work Order",
|
||||
"options": "Work Order"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-barcode",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-20 20:50:16.660433",
|
||||
"modified": "2021-01-08 14:31:15.375996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No",
|
||||
|
@ -473,16 +473,13 @@ def get_serial_nos(serial_no):
|
||||
if s.strip()]
|
||||
|
||||
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
|
||||
serial_no_doc.update({
|
||||
"item_code": args.get("item_code"),
|
||||
"company": args.get("company"),
|
||||
"batch_no": args.get("batch_no"),
|
||||
"via_stock_ledger": args.get("via_stock_ledger") or True,
|
||||
"supplier": args.get("supplier"),
|
||||
"location": args.get("location"),
|
||||
"warehouse": (args.get("warehouse")
|
||||
if args.get("actual_qty", 0) > 0 else None)
|
||||
})
|
||||
for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
|
||||
if args.get(field):
|
||||
serial_no_doc.set(field, args.get(field))
|
||||
|
||||
serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
|
||||
serial_no_doc.warehouse = (args.get("warehouse")
|
||||
if args.get("actual_qty", 0) > 0 else None)
|
||||
|
||||
if is_new:
|
||||
serial_no_doc.serial_no = serial_no
|
||||
|
@ -498,6 +498,7 @@ class StockEntry(StockController):
|
||||
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
|
||||
if not d.t_warehouse:
|
||||
outgoing_items_cost += flt(d.basic_amount)
|
||||
|
||||
return outgoing_items_cost
|
||||
|
||||
def get_args_for_incoming_rate(self, item):
|
||||
@ -854,6 +855,7 @@ class StockEntry(StockController):
|
||||
pro_doc.run_method("update_work_order_qty")
|
||||
if self.purpose == "Manufacture":
|
||||
pro_doc.run_method("update_planned_qty")
|
||||
pro_doc.update_batch_produced_qty(self)
|
||||
|
||||
if not pro_doc.operations:
|
||||
pro_doc.set_actual_dates()
|
||||
@ -1076,18 +1078,54 @@ class StockEntry(StockController):
|
||||
# in case of BOM
|
||||
to_warehouse = item.get("default_warehouse")
|
||||
|
||||
args = {
|
||||
"to_warehouse": to_warehouse,
|
||||
"from_warehouse": "",
|
||||
"qty": self.fg_completed_qty,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"stock_uom": item.stock_uom,
|
||||
"expense_account": item.get("expense_account"),
|
||||
"cost_center": item.get("buying_cost_center"),
|
||||
"is_finished_item": 1
|
||||
}
|
||||
|
||||
if self.work_order and self.pro_doc.has_batch_no:
|
||||
self.set_batchwise_finished_goods(args, item)
|
||||
else:
|
||||
self.add_finisged_goods(args, item)
|
||||
|
||||
def set_batchwise_finished_goods(self, args, item):
|
||||
qty = flt(self.fg_completed_qty)
|
||||
filters = {
|
||||
"reference_name": self.pro_doc.name,
|
||||
"reference_doctype": self.pro_doc.doctype,
|
||||
"qty_to_produce": (">", 0)
|
||||
}
|
||||
|
||||
fields = ["qty_to_produce as qty", "produced_qty", "name"]
|
||||
|
||||
for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"):
|
||||
batch_qty = flt(row.qty) - flt(row.produced_qty)
|
||||
if not batch_qty:
|
||||
continue
|
||||
|
||||
if qty <=0:
|
||||
break
|
||||
|
||||
fg_qty = batch_qty
|
||||
if batch_qty >= qty:
|
||||
fg_qty = qty
|
||||
|
||||
qty -= batch_qty
|
||||
args["qty"] = fg_qty
|
||||
args["batch_no"] = row.name
|
||||
|
||||
self.add_finisged_goods(args, item)
|
||||
|
||||
def add_finisged_goods(self, args, item):
|
||||
self.add_to_stock_entry_detail({
|
||||
item.name: {
|
||||
"to_warehouse": to_warehouse,
|
||||
"from_warehouse": "",
|
||||
"qty": self.fg_completed_qty,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"stock_uom": item.stock_uom,
|
||||
"expense_account": item.get("expense_account"),
|
||||
"cost_center": item.get("buying_cost_center"),
|
||||
"is_finished_item": 1
|
||||
}
|
||||
item.name: args
|
||||
}, bom_no = self.bom_no)
|
||||
|
||||
def get_bom_raw_materials(self, qty):
|
||||
@ -1524,6 +1562,36 @@ class StockEntry(StockController):
|
||||
material_requests.append(material_request)
|
||||
frappe.db.set_value('Material Request', material_request, 'transfer_status', status)
|
||||
|
||||
def set_serial_no_batch_for_finished_good(self):
|
||||
args = {}
|
||||
if self.pro_doc.serial_no:
|
||||
self.get_serial_nos_for_fg(args)
|
||||
|
||||
for row in self.items:
|
||||
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
|
||||
if args.get("serial_no"):
|
||||
row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)])
|
||||
|
||||
def get_serial_nos_for_fg(self, args):
|
||||
fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`",
|
||||
"`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"]
|
||||
|
||||
filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"],
|
||||
["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]]
|
||||
|
||||
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
|
||||
|
||||
if self.pro_doc.serial_no:
|
||||
args["serial_no"] = self.get_available_serial_nos(stock_entries)
|
||||
|
||||
def get_available_serial_nos(self, stock_entries):
|
||||
used_serial_nos = []
|
||||
for row in stock_entries:
|
||||
if row.serial_no:
|
||||
used_serial_nos.extend(get_serial_nos(row.serial_no))
|
||||
|
||||
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def move_sample_to_retention_warehouse(company, items):
|
||||
if isinstance(items, string_types):
|
||||
@ -1635,6 +1703,10 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
if bom.quantity:
|
||||
operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity)
|
||||
|
||||
if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings',
|
||||
'add_corrective_operation_cost_in_finished_good_valuation')):
|
||||
operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
|
||||
|
||||
return operating_cost_per_unit
|
||||
|
||||
def get_used_alternative_items(purchase_order=None, work_order=None):
|
||||
|
@ -18,6 +18,7 @@
|
||||
"col_break2",
|
||||
"is_finished_item",
|
||||
"is_scrap_item",
|
||||
"quality_inspection",
|
||||
"subcontracted_item",
|
||||
"section_break_8",
|
||||
"description",
|
||||
@ -69,7 +70,6 @@
|
||||
"putaway_rule",
|
||||
"column_break_51",
|
||||
"reference_purchase_receipt",
|
||||
"quality_inspection",
|
||||
"job_card_item"
|
||||
],
|
||||
"fields": [
|
||||
@ -548,7 +548,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-11 13:47:50.158754",
|
||||
"modified": "2021-04-22 20:08:23.799715",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
@ -48,37 +48,54 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
},
|
||||
|
||||
get_items: function(frm) {
|
||||
frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1,
|
||||
let fields = [{
|
||||
label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1,
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"company": frm.doc.company,
|
||||
}
|
||||
}
|
||||
}},
|
||||
function(data) {
|
||||
frappe.call({
|
||||
method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
|
||||
args: {
|
||||
warehouse: data.warehouse,
|
||||
posting_date: frm.doc.posting_date,
|
||||
posting_time: frm.doc.posting_time,
|
||||
company:frm.doc.company
|
||||
},
|
||||
callback: function(r) {
|
||||
var items = [];
|
||||
frm.clear_table("items");
|
||||
for(var i=0; i< r.message.length; i++) {
|
||||
var d = frm.add_child("items");
|
||||
$.extend(d, r.message[i]);
|
||||
if(!d.qty) d.qty = null;
|
||||
if(!d.valuation_rate) d.valuation_rate = null;
|
||||
}
|
||||
frm.refresh_field("items");
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
, __("Get Items"), __("Update"));
|
||||
}, {
|
||||
label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"disabled": 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
}];
|
||||
|
||||
frappe.prompt(fields, function(data) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
|
||||
args: {
|
||||
warehouse: data.warehouse,
|
||||
posting_date: frm.doc.posting_date,
|
||||
posting_time: frm.doc.posting_time,
|
||||
company: frm.doc.company,
|
||||
item_code: data.item_code
|
||||
},
|
||||
callback: function(r) {
|
||||
frm.clear_table("items");
|
||||
for (var i=0; i<r.message.length; i++) {
|
||||
var d = frm.add_child("items");
|
||||
$.extend(d, r.message[i]);
|
||||
|
||||
if (!d.qty) {
|
||||
d.qty = 0;
|
||||
}
|
||||
|
||||
if (!d.valuation_rate) {
|
||||
d.valuation_rate = 0;
|
||||
}
|
||||
}
|
||||
frm.refresh_field("items");
|
||||
}
|
||||
});
|
||||
}, __("Get Items"), __("Update"));
|
||||
},
|
||||
|
||||
posting_date: function(frm) {
|
||||
|
@ -481,45 +481,99 @@ class StockReconciliation(StockController):
|
||||
self._cancel()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items(warehouse, posting_date, posting_time, company):
|
||||
def get_items(warehouse, posting_date, posting_time, company, item_code=None):
|
||||
items = [frappe._dict({
|
||||
'item_code': item_code,
|
||||
'warehouse': warehouse
|
||||
})]
|
||||
|
||||
if not item_code:
|
||||
items = get_items_for_stock_reco(warehouse, company)
|
||||
|
||||
res = []
|
||||
itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
|
||||
|
||||
for d in items:
|
||||
if d.item_code in itemwise_batch_data:
|
||||
stock_bal = get_stock_balance(d.item_code, d.warehouse,
|
||||
posting_date, posting_time, with_valuation_rate=True)
|
||||
|
||||
for row in itemwise_batch_data.get(d.item_code):
|
||||
args = get_item_data(row, row.qty, stock_bal[1])
|
||||
res.append(args)
|
||||
else:
|
||||
stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time,
|
||||
with_valuation_rate=True , with_serial_no=cint(d.has_serial_no))
|
||||
|
||||
args = get_item_data(d, stock_bal[0], stock_bal[1],
|
||||
stock_bal[2] if cint(d.has_serial_no) else '')
|
||||
|
||||
res.append(args)
|
||||
|
||||
return res
|
||||
|
||||
def get_items_for_stock_reco(warehouse, company):
|
||||
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||
items = frappe.db.sql("""
|
||||
select i.name, i.item_name, bin.warehouse, i.has_serial_no
|
||||
select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no
|
||||
from tabBin bin, tabItem i
|
||||
where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1
|
||||
and i.has_variants = 0 and i.has_batch_no = 0
|
||||
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse)
|
||||
""", (lft, rgt))
|
||||
where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1
|
||||
and i.has_variants = 0 and exists(
|
||||
select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse
|
||||
)
|
||||
""", (lft, rgt), as_dict=1)
|
||||
|
||||
items += frappe.db.sql("""
|
||||
select i.name, i.item_name, id.default_warehouse, i.has_serial_no
|
||||
select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no
|
||||
from tabItem i, `tabItem Default` id
|
||||
where i.name = id.parent
|
||||
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse)
|
||||
and i.is_stock_item = 1 and i.has_batch_no = 0
|
||||
and i.has_variants = 0 and i.disabled = 0 and id.company=%s
|
||||
and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s
|
||||
group by i.name
|
||||
""", (lft, rgt, company))
|
||||
""", (lft, rgt, company), as_dict=1)
|
||||
|
||||
res = []
|
||||
for d in set(items):
|
||||
stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time,
|
||||
with_valuation_rate=True , with_serial_no=cint(d[3]))
|
||||
return items
|
||||
|
||||
if frappe.db.get_value("Item", d[0], "disabled") == 0:
|
||||
res.append({
|
||||
"item_code": d[0],
|
||||
"warehouse": d[2],
|
||||
"qty": stock_bal[0],
|
||||
"item_name": d[1],
|
||||
"valuation_rate": stock_bal[1],
|
||||
"current_qty": stock_bal[0],
|
||||
"current_valuation_rate": stock_bal[1],
|
||||
"current_serial_no": stock_bal[2] if cint(d[3]) else '',
|
||||
"serial_no": stock_bal[2] if cint(d[3]) else ''
|
||||
})
|
||||
def get_item_data(row, qty, valuation_rate, serial_no=None):
|
||||
return {
|
||||
'item_code': row.item_code,
|
||||
'warehouse': row.warehouse,
|
||||
'qty': qty,
|
||||
'item_name': row.item_name,
|
||||
'valuation_rate': valuation_rate,
|
||||
'current_qty': qty,
|
||||
'current_valuation_rate': valuation_rate,
|
||||
'current_serial_no': serial_no,
|
||||
'serial_no': serial_no,
|
||||
'batch_no': row.get('batch_no')
|
||||
}
|
||||
|
||||
return res
|
||||
def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
|
||||
from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute
|
||||
itemwise_batch_data = {}
|
||||
|
||||
filters = frappe._dict({
|
||||
'warehouse': warehouse,
|
||||
'from_date': posting_date,
|
||||
'to_date': posting_date,
|
||||
'company': company
|
||||
})
|
||||
|
||||
if item_code:
|
||||
filters.item_code = item_code
|
||||
|
||||
columns, data = execute(filters)
|
||||
|
||||
for row in data:
|
||||
itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({
|
||||
'item_code': row[0],
|
||||
'warehouse': warehouse,
|
||||
'qty': row[8],
|
||||
'item_name': row[1],
|
||||
'batch_no': row[4]
|
||||
}))
|
||||
|
||||
return itemwise_batch_data
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_balance_for(item_code, warehouse,
|
||||
|
@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Incorrect Balance Qty After Transaction"] = {
|
||||
"filters": [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
fieldname: "company",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __('Item Code'),
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'item_code',
|
||||
options: 'Item'
|
||||
},
|
||||
{
|
||||
label: __('Warehouse'),
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'warehouse'
|
||||
}
|
||||
]
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-05-12 16:47:58.717853",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-05-12 16:48:28.347575",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Incorrect Balance Qty After Transaction",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Stock Ledger Entry",
|
||||
"report_name": "Incorrect Balance Qty After Transaction",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from six import iteritems
|
||||
from frappe.utils import flt
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
return columns, data
|
||||
|
||||
def get_data(filters):
|
||||
data = get_stock_ledger_entries(filters)
|
||||
itewise_balance_qty = {}
|
||||
|
||||
for row in data:
|
||||
key = (row.item_code, row.warehouse)
|
||||
itewise_balance_qty.setdefault(key, []).append(row)
|
||||
|
||||
res = validate_data(itewise_balance_qty)
|
||||
return res
|
||||
|
||||
def validate_data(itewise_balance_qty):
|
||||
res = []
|
||||
for key, data in iteritems(itewise_balance_qty):
|
||||
row = get_incorrect_data(data)
|
||||
if row:
|
||||
res.append(row)
|
||||
res.append({})
|
||||
|
||||
return res
|
||||
|
||||
def get_incorrect_data(data):
|
||||
balance_qty = 0.0
|
||||
for row in data:
|
||||
balance_qty += row.actual_qty
|
||||
if row.voucher_type == "Stock Reconciliation" and not row.batch_no:
|
||||
balance_qty = flt(row.qty_after_transaction)
|
||||
|
||||
row.expected_balance_qty = balance_qty
|
||||
if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5:
|
||||
row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction))
|
||||
return row
|
||||
|
||||
def get_stock_ledger_entries(report_filters):
|
||||
filters = {}
|
||||
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty',
|
||||
'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no']
|
||||
|
||||
for field in ['warehouse', 'item_code', 'company']:
|
||||
if report_filters.get(field):
|
||||
filters[field] = report_filters.get(field)
|
||||
|
||||
return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
|
||||
order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
|
||||
|
||||
def get_columns():
|
||||
return [{
|
||||
'label': _('Id'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'name',
|
||||
'options': 'Stock Ledger Entry',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Posting Date'),
|
||||
'fieldtype': 'Date',
|
||||
'fieldname': 'posting_date',
|
||||
'width': 110
|
||||
}, {
|
||||
'label': _('Voucher Type'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'voucher_type',
|
||||
'options': 'DocType',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Voucher No'),
|
||||
'fieldtype': 'Dynamic Link',
|
||||
'fieldname': 'voucher_no',
|
||||
'options': 'voucher_type',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Item Code'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'item_code',
|
||||
'options': 'Item',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Warehouse'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'warehouse',
|
||||
'options': 'Warehouse',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Expected Balance Qty'),
|
||||
'fieldtype': 'Float',
|
||||
'fieldname': 'expected_balance_qty',
|
||||
'width': 170
|
||||
}, {
|
||||
'label': _('Actual Balance Qty'),
|
||||
'fieldtype': 'Float',
|
||||
'fieldname': 'qty_after_transaction',
|
||||
'width': 150
|
||||
}, {
|
||||
'label': _('Difference'),
|
||||
'fieldtype': 'Float',
|
||||
'fieldname': 'differnce',
|
||||
'width': 110
|
||||
}]
|
@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Incorrect Serial No Valuation"] = {
|
||||
"filters": [
|
||||
{
|
||||
label: __('Item Code'),
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'item_code',
|
||||
options: 'Item',
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
'has_serial_no': 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __('From Date'),
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'from_date',
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("year_start_date")
|
||||
},
|
||||
{
|
||||
label: __('To Date'),
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'to_date',
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("year_end_date")
|
||||
}
|
||||
]
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-05-13 13:07:00.767845",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"json": "{}",
|
||||
"modified": "2021-05-13 13:07:00.767845",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Incorrect Serial No Valuation",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Stock Ledger Entry",
|
||||
"report_name": "Incorrect Serial No Valuation",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
import copy
|
||||
from frappe import _
|
||||
from six import iteritems
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
return columns, data
|
||||
|
||||
def get_data(filters):
|
||||
data = get_stock_ledger_entries(filters)
|
||||
serial_nos_data = prepare_serial_nos(data)
|
||||
data = get_incorrect_serial_nos(serial_nos_data)
|
||||
|
||||
return data
|
||||
|
||||
def prepare_serial_nos(data):
|
||||
serial_no_wise_data = {}
|
||||
for row in data:
|
||||
if not row.serial_nos:
|
||||
continue
|
||||
|
||||
for serial_no in get_serial_nos(row.serial_nos):
|
||||
sle = copy.deepcopy(row)
|
||||
sle.serial_no = serial_no
|
||||
sle.qty = 1 if sle.actual_qty > 0 else -1
|
||||
sle.valuation_rate = sle.valuation_rate if sle.actual_qty > 0 else sle.valuation_rate * -1
|
||||
serial_no_wise_data.setdefault(serial_no, []).append(sle)
|
||||
|
||||
return serial_no_wise_data
|
||||
|
||||
def get_incorrect_serial_nos(serial_nos_data):
|
||||
result = []
|
||||
|
||||
total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))})
|
||||
|
||||
for serial_no, data in iteritems(serial_nos_data):
|
||||
total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))})
|
||||
|
||||
if check_incorrect_serial_data(data, total_dict):
|
||||
result.extend(data)
|
||||
|
||||
total_value.qty += total_dict.qty
|
||||
total_value.valuation_rate += total_dict.valuation_rate
|
||||
|
||||
result.append(total_dict)
|
||||
result.append({})
|
||||
|
||||
result.append(total_value)
|
||||
|
||||
return result
|
||||
|
||||
def check_incorrect_serial_data(data, total_dict):
|
||||
incorrect_data = False
|
||||
for row in data:
|
||||
total_dict.qty += row.qty
|
||||
total_dict.valuation_rate += row.valuation_rate
|
||||
|
||||
if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0):
|
||||
incorrect_data = True
|
||||
|
||||
return incorrect_data
|
||||
|
||||
def get_stock_ledger_entries(report_filters):
|
||||
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
|
||||
'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
|
||||
|
||||
filters = {'serial_no': ("is", "set")}
|
||||
|
||||
if report_filters.get('item_code'):
|
||||
filters['item_code'] = report_filters.get('item_code')
|
||||
|
||||
if report_filters.get('from_date') and report_filters.get('to_date'):
|
||||
filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')])
|
||||
|
||||
return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
|
||||
order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
|
||||
|
||||
def get_columns():
|
||||
return [{
|
||||
'label': _('Company'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'company',
|
||||
'options': 'Company',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Id'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'name',
|
||||
'options': 'Stock Ledger Entry',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Posting Date'),
|
||||
'fieldtype': 'Date',
|
||||
'fieldname': 'posting_date',
|
||||
'width': 90
|
||||
}, {
|
||||
'label': _('Posting Time'),
|
||||
'fieldtype': 'Time',
|
||||
'fieldname': 'posting_time',
|
||||
'width': 90
|
||||
}, {
|
||||
'label': _('Voucher Type'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'voucher_type',
|
||||
'options': 'DocType',
|
||||
'width': 100
|
||||
}, {
|
||||
'label': _('Voucher No'),
|
||||
'fieldtype': 'Dynamic Link',
|
||||
'fieldname': 'voucher_no',
|
||||
'options': 'voucher_type',
|
||||
'width': 110
|
||||
}, {
|
||||
'label': _('Item Code'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'item_code',
|
||||
'options': 'Item',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Warehouse'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'warehouse',
|
||||
'options': 'Warehouse',
|
||||
'width': 120
|
||||
}, {
|
||||
'label': _('Serial No'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'serial_no',
|
||||
'options': 'Serial No',
|
||||
'width': 100
|
||||
}, {
|
||||
'label': _('Qty'),
|
||||
'fieldtype': 'Float',
|
||||
'fieldname': 'qty',
|
||||
'width': 80
|
||||
}, {
|
||||
'label': _('Valuation Rate (In / Out)'),
|
||||
'fieldtype': 'Currency',
|
||||
'fieldname': 'valuation_rate',
|
||||
'width': 110
|
||||
}]
|
@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Incorrect Stock Value Report"] = {
|
||||
"filters": [
|
||||
{
|
||||
"label": __("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"default": frappe.defaults.get_user_default("Company")
|
||||
},
|
||||
{
|
||||
"label": __("Account"),
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
get_query: function() {
|
||||
var company = frappe.query_report.get_filter_value('company');
|
||||
return {
|
||||
filters: {
|
||||
"account_type": "Stock",
|
||||
"company": company
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": __("From Date"),
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date"
|
||||
}
|
||||
]
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-06-22 15:35:05.148177",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-06-22 15:35:05.148177",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Incorrect Stock Value Report",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Stock Ledger Entry",
|
||||
"report_name": "Incorrect Stock Value Report",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import erpnext
|
||||
from frappe import _
|
||||
from six import iteritems
|
||||
from frappe.utils import add_days, today, getdate
|
||||
from erpnext.stock.utils import get_stock_value_on
|
||||
from erpnext.accounts.utils import get_stock_and_account_balance
|
||||
|
||||
def execute(filters=None):
|
||||
if not erpnext.is_perpetual_inventory_enabled(filters.company):
|
||||
frappe.throw(_("Perpetual inventory required for the company {0} to view this report.")
|
||||
.format(filters.company))
|
||||
|
||||
data = get_data(filters)
|
||||
columns = get_columns(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_unsync_date(filters):
|
||||
date = filters.from_date
|
||||
if not date:
|
||||
date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""")
|
||||
date = date[0][0]
|
||||
|
||||
if not date:
|
||||
return
|
||||
|
||||
while getdate(date) < getdate(today()):
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date,
|
||||
company=filters.company, account = filters.account)
|
||||
|
||||
if abs(account_bal - stock_bal) > 0.1:
|
||||
return date
|
||||
|
||||
date = add_days(date, 1)
|
||||
|
||||
def get_data(report_filters):
|
||||
from_date = get_unsync_date(report_filters)
|
||||
|
||||
if not from_date:
|
||||
return []
|
||||
|
||||
result = []
|
||||
|
||||
voucher_wise_dict = {}
|
||||
data = frappe.db.sql('''
|
||||
SELECT
|
||||
name, posting_date, posting_time, voucher_type, voucher_no,
|
||||
stock_value_difference, stock_value, warehouse, item_code
|
||||
FROM
|
||||
`tabStock Ledger Entry`
|
||||
WHERE
|
||||
posting_date
|
||||
= %s and company = %s
|
||||
and is_cancelled = 0
|
||||
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
|
||||
''', (from_date, report_filters.company), as_dict=1)
|
||||
|
||||
for d in data:
|
||||
voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)
|
||||
|
||||
closing_date = add_days(from_date, -1)
|
||||
for key, stock_data in iteritems(voucher_wise_dict):
|
||||
prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1])
|
||||
for data in stock_data:
|
||||
expected_stock_value = prev_stock_value + data.stock_value_difference
|
||||
if abs(data.stock_value - expected_stock_value) > 0.1:
|
||||
data.difference_value = abs(data.stock_value - expected_stock_value)
|
||||
data.expected_stock_value = expected_stock_value
|
||||
result.append(data)
|
||||
|
||||
return result
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Stock Ledger ID"),
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"options": "Stock Ledger Entry",
|
||||
"width": "80"
|
||||
},
|
||||
{
|
||||
"label": _("Posting Date"),
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"label": _("Posting Time"),
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time"
|
||||
},
|
||||
{
|
||||
"label": _("Voucher Type"),
|
||||
"fieldname": "voucher_type",
|
||||
"width": "110"
|
||||
},
|
||||
{
|
||||
"label": _("Voucher No"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
"width": "110"
|
||||
},
|
||||
{
|
||||
"label": _("Item Code"),
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": "110"
|
||||
},
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"options": "Warehouse",
|
||||
"width": "110"
|
||||
},
|
||||
{
|
||||
"label": _("Expected Stock Value"),
|
||||
"fieldname": "expected_stock_value",
|
||||
"fieldtype": "Currency",
|
||||
"width": "150"
|
||||
},
|
||||
{
|
||||
"label": _("Stock Value"),
|
||||
"fieldname": "stock_value",
|
||||
"fieldtype": "Currency",
|
||||
"width": "120"
|
||||
},
|
||||
{
|
||||
"label": _("Difference Value"),
|
||||
"fieldname": "difference_value",
|
||||
"fieldtype": "Currency",
|
||||
"width": "150"
|
||||
}
|
||||
]
|
@ -15,6 +15,7 @@
|
||||
"hide_custom": 0,
|
||||
"icon": "stock",
|
||||
"idx": 0,
|
||||
"is_default": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Stock",
|
||||
"links": [
|
||||
@ -653,9 +654,44 @@
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Incorrect Data Report",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Incorrect Serial No Qty and Valuation",
|
||||
"link_to": "Incorrect Serial No Valuation",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Incorrect Balance Qty After Transaction",
|
||||
"link_to": "Incorrect Balance Qty After Transaction",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Stock and Account Value Comparison",
|
||||
"link_to": "Stock and Account Value Comparison",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2020-12-01 13:38:36.282890",
|
||||
"modified": "2021-05-13 13:10:24.914983",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock",
|
||||
|
@ -166,7 +166,7 @@
|
||||
"options": "Service Level Agreement"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.status != 'Replied';",
|
||||
"depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
|
||||
"fieldname": "response_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Response By",
|
||||
@ -180,7 +180,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.status != 'Replied';",
|
||||
"depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
|
||||
"fieldname": "resolution_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Resolution By",
|
||||
@ -410,7 +410,7 @@
|
||||
"icon": "fa fa-ticket",
|
||||
"idx": 7,
|
||||
"links": [],
|
||||
"modified": "2021-05-26 10:49:07.574769",
|
||||
"modified": "2021-06-10 03:22:27.098898",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Support",
|
||||
"name": "Issue",
|
||||
|
@ -26,6 +26,9 @@ class Issue(Document):
|
||||
|
||||
self.set_lead_contact(self.raised_by)
|
||||
|
||||
if not self.service_level_agreement:
|
||||
self.reset_sla_fields()
|
||||
|
||||
def on_update(self):
|
||||
# Add a communication in the issue timeline
|
||||
if self.flags.create_communication and self.via_customer_portal:
|
||||
@ -51,6 +54,106 @@ class Issue(Document):
|
||||
self.company = frappe.db.get_value("Lead", self.lead, "company") or \
|
||||
frappe.db.get_default("Company")
|
||||
|
||||
def reset_sla_fields(self):
|
||||
self.agreement_status = ""
|
||||
self.response_by = ""
|
||||
self.resolution_by = ""
|
||||
self.response_by_variance = 0
|
||||
self.resolution_by_variance = 0
|
||||
|
||||
def update_status(self):
|
||||
status = frappe.db.get_value("Issue", self.name, "status")
|
||||
if self.status != "Open" and status == "Open" and not self.first_responded_on:
|
||||
self.first_responded_on = frappe.flags.current_time or now_datetime()
|
||||
|
||||
if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
|
||||
self.resolution_date = frappe.flags.current_time or now_datetime()
|
||||
if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing":
|
||||
set_service_level_agreement_variance(issue=self.name)
|
||||
self.update_agreement_status()
|
||||
set_resolution_time(issue=self)
|
||||
set_user_resolution_time(issue=self)
|
||||
|
||||
if self.status == "Open" and status != "Open":
|
||||
# if no date, it should be set as None and not a blank string "", as per mysql strict config
|
||||
self.resolution_date = None
|
||||
self.reset_issue_metrics()
|
||||
# enable SLA and variance on Reopen
|
||||
self.agreement_status = "Ongoing"
|
||||
set_service_level_agreement_variance(issue=self.name)
|
||||
|
||||
self.handle_hold_time(status)
|
||||
|
||||
def handle_hold_time(self, status):
|
||||
if self.service_level_agreement:
|
||||
# set response and resolution variance as None as the issue is on Hold
|
||||
pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"],
|
||||
filters={"parent": self.service_level_agreement})
|
||||
hold_statuses = [entry.status for entry in pause_sla_on]
|
||||
update_values = {}
|
||||
|
||||
if hold_statuses:
|
||||
if self.status in hold_statuses and status not in hold_statuses:
|
||||
update_values['on_hold_since'] = frappe.flags.current_time or now_datetime()
|
||||
if not self.first_responded_on:
|
||||
update_values['response_by'] = None
|
||||
update_values['response_by_variance'] = 0
|
||||
update_values['resolution_by'] = None
|
||||
update_values['resolution_by_variance'] = 0
|
||||
|
||||
# calculate hold time when status is changed from any hold status to any non-hold status
|
||||
if self.status not in hold_statuses and status in hold_statuses:
|
||||
hold_time = self.total_hold_time if self.total_hold_time else 0
|
||||
now_time = frappe.flags.current_time or now_datetime()
|
||||
last_hold_time = 0
|
||||
if self.on_hold_since:
|
||||
# last_hold_time will be added to the sla variables
|
||||
last_hold_time = time_diff_in_seconds(now_time, self.on_hold_since)
|
||||
update_values['total_hold_time'] = hold_time + last_hold_time
|
||||
|
||||
# re-calculate SLA variables after issue changes from any hold status to any non-hold status
|
||||
# add hold time to SLA variables
|
||||
start_date_time = get_datetime(self.service_level_agreement_creation)
|
||||
priority = get_priority(self)
|
||||
now_time = frappe.flags.current_time or now_datetime()
|
||||
|
||||
if not self.first_responded_on:
|
||||
response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
|
||||
response_by = add_to_date(response_by, seconds=round(last_hold_time))
|
||||
response_by_variance = round(time_diff_in_seconds(response_by, now_time))
|
||||
update_values['response_by'] = response_by
|
||||
update_values['response_by_variance'] = response_by_variance + last_hold_time
|
||||
|
||||
resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
|
||||
resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
|
||||
resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
|
||||
update_values['resolution_by'] = resolution_by
|
||||
update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
|
||||
update_values['on_hold_since'] = None
|
||||
|
||||
self.db_set(update_values)
|
||||
|
||||
def update_agreement_status(self):
|
||||
if self.service_level_agreement and self.agreement_status == "Ongoing":
|
||||
if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \
|
||||
cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0:
|
||||
|
||||
self.agreement_status = "Failed"
|
||||
else:
|
||||
self.agreement_status = "Fulfilled"
|
||||
|
||||
def update_agreement_status_on_custom_status(self):
|
||||
"""
|
||||
Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status
|
||||
"""
|
||||
if not self.first_responded_on: # first_responded_on set when first reply is sent to customer
|
||||
self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2)
|
||||
|
||||
if not self.resolution_date: # resolution_date set when issue has been closed
|
||||
self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2)
|
||||
|
||||
self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
|
||||
|
||||
def create_communication(self):
|
||||
communication = frappe.new_doc("Communication")
|
||||
communication.update({
|
||||
@ -215,4 +318,4 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals
|
||||
def get_holidays(holiday_list_name):
|
||||
holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name)
|
||||
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
|
||||
return holidays
|
||||
return holidays
|
||||
|
Loading…
Reference in New Issue
Block a user