Merge branch 'develop' into asset-repair-refactor

This commit is contained in:
Saqib
2021-07-01 19:27:20 +05:30
committed by GitHub
89 changed files with 3009 additions and 736 deletions

View File

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

View File

@@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self): def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', 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) msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg) frappe.throw(msg)

View File

@@ -121,8 +121,7 @@ class GLEntry(Document):
def check_pl_account(self): def check_pl_account(self):
if self.is_opening=='Yes' and \ if self.is_opening=='Yes' and \
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \ frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))

View File

@@ -517,6 +517,8 @@ class PurchaseInvoice(BuyingController):
if d.category in ('Valuation', 'Total and Valuation') if d.category in ('Valuation', 'Total and Valuation')
and flt(d.base_tax_amount_after_discount_amount)] and flt(d.base_tax_amount_after_discount_amount)]
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
for item in self.get("items"): for item in self.get("items"):
if flt(item.base_net_amount): if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account) account_currency = get_account_currency(item.expense_account)
@@ -634,6 +636,34 @@ class PurchaseInvoice(BuyingController):
"project": item.project or self.project "project": item.project or self.project
}, account_currency, item=item)) }, account_currency, item=item))
# check if the exchange rate has changed
if item.get('purchase_receipt'):
if exchange_rate_map[item.purchase_receipt] and \
self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \
item.net_rate == net_rate_map[item.pr_detail]:
discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \
(exchange_rate_map[item.purchase_receipt] - self.conversion_rate)
gl_entries.append(
self.get_gl_dict({
"account": expense_account,
"against": self.supplier,
"debit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
gl_entries.append(
self.get_gl_dict({
"account": self.get_company_default("exchange_gain_loss_account"),
"against": self.supplier,
"credit": discrepancy_caused_by_exchange_rate_difference,
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
# If asset is bought through this document and not linked to PR # If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount: if self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@@ -1141,6 +1171,36 @@ class PurchaseInvoice(BuyingController):
if update: if update:
self.db_set('status', self.status, update_modified = update_modified) self.db_set('status', self.status, update_modified = update_modified)
# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling
def get_purchase_document_details(doc):
if doc.doctype == 'Purchase Invoice':
doc_reference = 'purchase_receipt'
items_reference = 'pr_detail'
parent_doctype = 'Purchase Receipt'
child_doctype = 'Purchase Receipt Item'
else:
doc_reference = 'purchase_invoice'
items_reference = 'purchase_invoice_item'
parent_doctype = 'Purchase Invoice'
child_doctype = 'Purchase Invoice Item'
purchase_receipts_or_invoices = []
items = []
for item in doc.get('items'):
if item.get(doc_reference):
purchase_receipts_or_invoices.append(item.get(doc_reference))
if item.get(items_reference):
items.append(item.get(items_reference))
exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in',
purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1))
net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in',
items)}, fields=['name', 'net_rate'], as_list=1))
return exchange_rate_map, net_rate_map
def get_list_context(context=None): def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context) list_context = get_list_context(context)

View File

@@ -230,6 +230,27 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit) self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_purchase_invoice_with_exchange_rate_difference(self):
pr = make_purchase_receipt(currency = "USD", conversion_rate = 70)
pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True")
pi.items[0].purchase_receipt = pr.name
pi.items[0].pr_detail = pr.items[0].name
pi.insert()
pi.submit()
# fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account
gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'})
voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
self.assertEqual(pi.name, voucher_no)
exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
def test_purchase_invoice_change_naming_series(self): def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1]) pi = frappe.copy_doc(test_records[1])
pi.insert() pi.insert()

View File

@@ -854,7 +854,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-16 19:33:51.099386", "modified": "2021-06-16 19:43:51.099386",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -1957,6 +1957,33 @@ class TestSalesInvoice(unittest.TestCase):
einvoice = make_einvoice(si) einvoice = make_einvoice(si)
validate_totals(einvoice) validate_totals(einvoice)
def test_item_tax_net_range(self):
item = create_item("T Shirt")
item.set('taxes', [])
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500
})
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000
})
item.save()
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
# Apply discount
sales_invoice.apply_discount_on = 'Net Total'
sales_invoice.discount_amount = 300
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def get_sales_invoice_for_e_invoice(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'
@@ -1985,33 +2012,6 @@ def get_sales_invoice_for_e_invoice():
return si return si
def test_item_tax_net_range(self):
item = create_item("T Shirt")
item.set('taxes', [])
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500
})
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000
})
item.save()
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
# Apply discount
sales_invoice.apply_discount_on = 'Net Total'
sales_invoice.discount_amount = 300
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def make_test_address_for_ewaybill(): def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({ address = frappe.get_doc({

View File

@@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
{ {
"fieldname":"account", "fieldname":"account",
"label": __("Account"), "label": __("Account"),
"fieldtype": "Link", "fieldtype": "MultiSelectList",
"options": "Account", "options": "Account",
"get_query": function() { get_data: function(txt) {
var company = frappe.query_report.get_filter_value('company'); return frappe.db.get_link_options('Account', txt, {
return { company: frappe.query_report.get_filter_value("company")
"doctype": "Account", });
"filters": {
"company": company,
}
}
} }
}, },
{ {
@@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Cost Center"), "label": __("Cost Center"),
"fieldtype": "MultiSelectList", "fieldtype": "MultiSelectList",
get_data: function(txt) { 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"), "label": __("Project"),
"fieldtype": "MultiSelectList", "fieldtype": "MultiSelectList",
get_data: function(txt) { 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")
});
} }
}, },
{ {

View File

@@ -49,8 +49,12 @@ def validate_filters(filters, account_details):
if not filters.get("from_date") and not filters.get("to_date"): 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")))) 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): for account in filters.account:
frappe.throw(_("Account {0} does not exists").format(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') if (filters.get("account") and filters.get("group_by") == _('Group by Account')
and account_details[filters.account].is_group == 0): and account_details[filters.account].is_group == 0):
@@ -87,7 +91,19 @@ def set_account_currency(filters):
account_currency = None account_currency = None
if filters.get("account"): 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"): elif filters.get("party"):
gle_currency = frappe.db.get_value( gle_currency = frappe.db.get_value(
"GL Entry", { "GL Entry", {
@@ -205,10 +221,10 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters): def get_conditions(filters):
conditions = [] conditions = []
if filters.get("account") and not filters.get("include_dimensions"):
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) if filters.get("account"):
conditions.append("""account in (select name from tabAccount filters.account = get_accounts_with_children(filters.account)
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) conditions.append("account in %(account)s")
if filters.get("cost_center"): if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.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 "" 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): def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = [] data = []

View File

@@ -123,7 +123,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-06-23 19:40:00.120822", "modified": "2021-06-24 10:38:28.934525",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View 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))

View File

@@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
import erpnext import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map 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.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
@@ -497,9 +497,6 @@ class StockController(AccountsController):
}) })
if future_sle_exists(args): if future_sle_exists(args):
create_repost_item_valuation_entry(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() @frappe.whitelist()
def make_quality_inspections(doctype, docname, items): def make_quality_inspections(doctype, docname, items):

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
import json import json
from frappe.utils import getdate from frappe.utils import getdate, strip_html
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
class TestPatientHistorySettings(unittest.TestCase): class TestPatientHistorySettings(unittest.TestCase):
@@ -44,9 +44,9 @@ class TestPatientHistorySettings(unittest.TestCase):
self.assertTrue(medical_rec) self.assertTrue(medical_rec)
medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
expected_subject = "<b>Date: </b>{0}<br><b>Rating: </b>3<br><b>Feedback: </b>Test Patient History Settings<br>".format( expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format(
frappe.utils.format_date(getdate())) frappe.utils.format_date(getdate()))
self.assertEqual(medical_rec.subject, expected_subject) self.assertEqual(strip_html(medical_rec.subject), expected_subject)
self.assertEqual(medical_rec.patient, patient) self.assertEqual(medical_rec.patient, patient)
self.assertEqual(medical_rec.communication_date, getdate()) self.assertEqual(medical_rec.communication_date, getdate())

View File

@@ -158,6 +158,7 @@ website_route_rules = [
"parents": [{"label": _("Material Request"), "route": "material-requests"}] "parents": [{"label": _("Material Request"), "route": "material-requests"}]
} }
}, },
{"from_route": "/project", "to_route": "Project"}
] ]
standard_portal_menu_items = [ standard_portal_menu_items = [

View File

@@ -15,6 +15,7 @@ class Attendance(Document):
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
self.validate_attendance_date() self.validate_attendance_date()
self.validate_duplicate_record() self.validate_duplicate_record()
self.validate_employee_status()
self.check_leave_record() self.check_leave_record()
def validate_attendance_date(self): def validate_attendance_date(self):
@@ -38,6 +39,10 @@ class Attendance(Document):
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date))) frappe.bold(self.employee), frappe.bold(self.attendance_date)))
def validate_employee_status(self):
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
def check_leave_record(self): def check_leave_record(self):
leave_record = frappe.db.sql(""" leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date select leave_type, half_day, half_day_date

View File

@@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = {
label: __('For Employee'), label: __('For Employee'),
fieldtype: 'Link', fieldtype: 'Link',
options: 'Employee', options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"}
},
reqd: 1, reqd: 1,
onchange: function() { onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1); dialog.set_df_property("unmarked_days", "hidden", 1);

View File

@@ -110,7 +110,7 @@
"label": "Allocation" "label": "Allocation"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 1, "bold": 1,
"fieldname": "new_leaves_allocated", "fieldname": "new_leaves_allocated",
"fieldtype": "Float", "fieldtype": "Float",

View File

@@ -164,7 +164,6 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.cancel() leave_allocation.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
def test_leave_addition_after_submit(self): def test_leave_addition_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`") frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`") frappe.db.sql("delete from `tabLeave Ledger Entry`")
@@ -179,7 +178,6 @@ class TestLeaveAllocation(unittest.TestCase):
def test_leave_subtraction_after_submit(self): def test_leave_subtraction_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`") frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`") frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation() leave_allocation = create_leave_allocation()
leave_allocation.submit() leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15) self.assertTrue(leave_allocation.total_leaves_allocated, 15)
@@ -199,8 +197,8 @@ class TestLeaveAllocation(unittest.TestCase):
"doctype": 'Leave Application', "doctype": 'Leave Application',
"employee": employee.name, "employee": employee.name,
"leave_type": "_Test Leave Type", "leave_type": "_Test Leave Type",
"from_date": nowdate(), "from_date": add_months(nowdate(), 2),
"to_date": add_days(nowdate(), 10), "to_date": add_months(add_days(nowdate(), 10), 2),
"company": erpnext.get_default_company() or "_Test Company", "company": erpnext.get_default_company() or "_Test Company",
"docstatus": 1, "docstatus": 1,
"status": "Approved", "status": "Approved",

View File

@@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", {
refresh: function(frm) { refresh: function(frm) {
frm.toggle_enable("item", frm.doc.__islocal); frm.toggle_enable("item", frm.doc.__islocal);
toggle_operations(frm);
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
function(doc) { function(doc) {
@@ -326,8 +325,7 @@ frappe.ui.form.on("BOM", {
freeze: true, freeze: true,
args: { args: {
update_parent: true, update_parent: true,
from_child_bom:false, from_child_bom:false
save: frm.doc.docstatus === 1 ? true : false
}, },
callback: function(r) { callback: function(r) {
refresh_field("items"); refresh_field("items");
@@ -651,15 +649,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) {
erpnext.bom.calculate_total(frm.doc); 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) { frappe.ui.form.on("BOM", "with_operations", function(frm) {
if(!cint(frm.doc.with_operations)) { if(!cint(frm.doc.with_operations)) {
frm.set_value("operations", []); frm.set_value("operations", []);
} }
toggle_operations(frm);
}); });

View File

@@ -193,6 +193,7 @@
}, },
{ {
"default": "Work Order", "default": "Work Order",
"depends_on": "with_operations",
"fieldname": "transfer_material_against", "fieldname": "transfer_material_against",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Transfer Material Against", "label": "Transfer Material Against",
@@ -235,6 +236,7 @@
{ {
"fieldname": "operations_section", "fieldname": "operations_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
@@ -245,6 +247,7 @@
"options": "Routing" "options": "Routing"
}, },
{ {
"depends_on": "with_operations",
"fieldname": "operations", "fieldname": "operations",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Operations", "label": "Operations",
@@ -517,7 +520,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-21 12:29:32.634952", "modified": "2021-03-16 12:25:09.081968",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -662,7 +662,7 @@ class BOM(WebsiteGenerator):
self.get_routing() self.get_routing()
def validate_operations(self): 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")) frappe.throw(_("Operations cannot be left blank"))
if self.with_operations: if self.with_operations:

View File

@@ -13,10 +13,10 @@
"col_break1", "col_break1",
"hour_rate", "hour_rate",
"time_in_mins", "time_in_mins",
"batch_size",
"operating_cost", "operating_cost",
"base_hour_rate", "base_hour_rate",
"base_operating_cost", "base_operating_cost",
"batch_size",
"image" "image"
], ],
"fields": [ "fields": [
@@ -61,6 +61,8 @@
}, },
{ {
"description": "In minutes", "description": "In minutes",
"fetch_from": "operation.total_operation_time",
"fetch_if_empty": 1,
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
@@ -104,7 +106,8 @@
"label": "Image" "label": "Image"
}, },
{ {
"default": "1", "fetch_from": "operation.batch_size",
"fetch_if_empty": 1,
"fieldname": "batch_size", "fieldname": "batch_size",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Batch Size" "label": "Batch Size"
@@ -120,7 +123,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-10-13 18:14:10.018774", "modified": "2021-01-12 14:48:09.596843",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operation", "name": "BOM Operation",

View File

@@ -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) { 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() { frm.set_query("quality_inspection", function() {
return { return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", 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"); 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.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons"); 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) { operation: function(frm) {
frm.trigger("toggle_operation_number"); frm.trigger("toggle_operation_number");
@@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
prepare_timer_buttons: function(frm) { prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard"); frm.trigger("make_dashboard");
if (!frm.doc.job_started) {
frm.add_custom_button(__("Start"), () => { if (!frm.doc.started_time && !frm.doc.current_time) {
if (!frm.doc.employee) { frm.add_custom_button(__("Start Job"), () => {
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
fieldname: 'employee'}, d => { frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
if (d.employee) { options: "Job Card Time Log", fieldname: 'employees'}, d => {
frm.set_value("employee", d.employee); frm.events.start_job(frm, "Work In Progress", d.employees);
} else { }, __("Assign Job to Employee"));
frm.events.start_job(frm);
}
}, __("Enter Value"), __("Start"));
} else { } else {
frm.events.start_job(frm); frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
} }
}).addClass("btn-primary"); }).addClass("btn-primary");
} else if (frm.doc.status == "On Hold") { } else if (frm.doc.status == "On Hold") {
frm.add_custom_button(__("Resume"), () => { frm.add_custom_button(__("Resume Job"), () => {
frappe.flags.resume_job = 1; frm.events.start_job(frm, "Resume Job", frm.doc.employee);
frm.events.start_job(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
} else { } else {
frm.add_custom_button(__("Pause"), () => { frm.add_custom_button(__("Pause Job"), () => {
frappe.flags.pause_job = 1; frm.events.complete_job(frm, "On Hold");
frm.set_value("status", "On Hold");
frm.events.complete_job(frm);
}); });
frm.add_custom_button(__("Complete"), () => { frm.add_custom_button(__("Complete Job"), () => {
let completed_time = frappe.datetime.now_datetime(); var sub_operations = frm.doc.sub_operations;
frm.trigger("hide_timer");
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'), frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { fieldname: 'qty', default: frm.doc.for_quantity}, data => {
frm.events.complete_job(frm, completed_time, data.qty); frm.events.complete_job(frm, "Complete", data.qty);
}, __("Enter Value"), __("Complete")); }, __("Enter Value"));
} else { } else {
frm.events.complete_job(frm, completed_time, 0); frm.events.complete_job(frm, "Complete", 0.0);
} }
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
}, },
start_job: function(frm) { start_job: function(frm, status, employee) {
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); const args = {
row.from_time = frappe.datetime.now_datetime(); job_card_id: frm.doc.name,
frm.set_value('job_started', 1); start_time: frappe.datetime.now_datetime(),
frm.set_value('started_time' , row.from_time); employees: employee,
frm.set_value("status", "Work In Progress"); status: status
};
if (!frappe.flags.resume_job) { frm.events.make_time_log(frm, args);
frm.set_value('current_time' , 0);
}
frm.save();
}, },
complete_job: function(frm, completed_time, completed_qty) { complete_job: function(frm, status, completed_qty) {
frm.doc.time_logs.forEach(d => { const args = {
if (d.from_time && !d.to_time) { job_card_id: frm.doc.name,
d.to_time = completed_time || frappe.datetime.now_datetime(); complete_time: frappe.datetime.now_datetime(),
d.completed_qty = completed_qty || 0; status: status,
completed_qty: completed_qty
};
frm.events.make_time_log(frm, args);
},
if(frappe.flags.pause_job) { make_time_log: function(frm, args) {
let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; frm.events.update_sub_operation(frm, args);
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);
}
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) { validate: function(frm) {
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) { if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
frm.trigger("reset_timer"); 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) { reset_timer: function(frm) {
frm.set_value('started_time' , ''); frm.set_value('started_time' , '');
frm.set_value('job_started', 0);
frm.set_value('current_time' , 0);
}, },
make_dashboard: function(frm) { make_dashboard: function(frm) {
@@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
}, },
to_time: function(frm) { to_time: function(frm) {
frm.set_value('job_started', 0);
frm.set_value('started_time', ''); frm.set_value('started_time', '');
} }
}) })

View File

@@ -9,38 +9,49 @@
"naming_series", "naming_series",
"work_order", "work_order",
"bom_no", "bom_no",
"workstation",
"operation",
"operation_row_number",
"column_break_4", "column_break_4",
"posting_date", "posting_date",
"company", "company",
"remarks",
"production_section", "production_section",
"production_item", "production_item",
"item_name", "item_name",
"for_quantity", "for_quantity",
"quality_inspection", "serial_no",
"wip_warehouse",
"column_break_12", "column_break_12",
"employee", "wip_warehouse",
"employee_name", "quality_inspection",
"status",
"project", "project",
"batch_no",
"operation_section_section",
"operation",
"operation_row_number",
"column_break_18",
"workstation",
"employee",
"section_break_21",
"sub_operations",
"timing_detail", "timing_detail",
"time_logs", "time_logs",
"section_break_13", "section_break_13",
"total_completed_qty", "total_completed_qty",
"total_time_in_mins",
"column_break_15", "column_break_15",
"total_time_in_mins",
"section_break_8", "section_break_8",
"items", "items",
"corrective_operation_section",
"for_job_card",
"is_corrective_job_card",
"column_break_33",
"hour_rate",
"for_operation",
"more_information", "more_information",
"operation_id", "operation_id",
"sequence_id", "sequence_id",
"transferred_qty", "transferred_qty",
"requested_qty", "requested_qty",
"status",
"column_break_20", "column_break_20",
"remarks",
"barcode", "barcode",
"job_started", "job_started",
"started_time", "started_time",
@@ -117,13 +128,6 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Timing Detail" "label": "Timing Detail"
}, },
{
"fieldname": "employee",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee"
},
{ {
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
"fieldname": "time_logs", "fieldname": "time_logs",
@@ -133,9 +137,11 @@
}, },
{ {
"fieldname": "section_break_13", "fieldname": "section_break_13",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"hide_border": 1
}, },
{ {
"default": "0",
"fieldname": "total_completed_qty", "fieldname": "total_completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Total Completed Qty", "label": "Total Completed Qty",
@@ -160,8 +166,7 @@
"fieldname": "items", "fieldname": "items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items", "label": "Items",
"options": "Job Card Item", "options": "Job Card Item"
"read_only": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -251,12 +256,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"fetch_from": "employee.employee_name", "collapsible": 1,
"fieldname": "employee_name",
"fieldtype": "Read Only",
"label": "Employee Name"
},
{
"fieldname": "production_section", "fieldname": "production_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Production" "label": "Production"
@@ -314,11 +314,89 @@
"label": "Quality Inspection", "label": "Quality Inspection",
"no_copy": 1, "no_copy": 1,
"options": "Quality Inspection" "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, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-19 18:26:50.531664", "modified": "2021-03-16 15:59:32.766484",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@@ -5,11 +5,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import datetime import datetime
import json
from frappe import _, bold from frappe import _, bold
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, 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 from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
@@ -25,10 +26,21 @@ class JobCard(Document):
self.set_status() self.set_status()
self.validate_operation_id() self.validate_operation_id()
self.validate_sequence_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): def validate_time_logs(self):
self.total_completed_qty = 0.0
self.total_time_in_mins = 0.0 self.total_time_in_mins = 0.0
self.total_completed_qty = 0.0
if self.get('time_logs'): if self.get('time_logs'):
for d in 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 d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
self.total_time_in_mins += d.time_in_mins 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 += d.completed_qty
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_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): def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1 production_capacity = 1
@@ -57,7 +72,7 @@ class JobCard(Document):
self.workstation, 'production_capacity') or 1 self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s " validate_overlap_for = " and jc.workstation = %(workstation)s "
if self.employee: if args.get("employee"):
# override capacity for employee # override capacity for employee
production_capacity = 1 production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s " validate_overlap_for = " and jc.employee = %(employee)s "
@@ -80,7 +95,7 @@ class JobCard(Document):
"to_time": args.to_time, "to_time": args.to_time,
"name": args.name or "No Name", "name": args.name or "No Name",
"parent": args.parent or "No Name", "parent": args.parent or "No Name",
"employee": self.employee, "employee": args.get("employee"),
"workstation": self.workstation "workstation": self.workstation
}, as_dict=True) }, as_dict=True)
@@ -158,6 +173,100 @@ class JobCard(Document):
row.planned_start_time = datetime.datetime.combine(start_date, row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time)) 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): def update_time_logs(self, row):
self.append("time_logs", { self.append("time_logs", {
"from_time": row.planned_start_time, "from_time": row.planned_start_time,
@@ -182,15 +291,18 @@ class JobCard(Document):
if self.get('operation') == d.operation: if self.get('operation') == d.operation:
self.append('items', { self.append('items', {
'item_code': d.item_code, "item_code": d.item_code,
'source_warehouse': d.source_warehouse, "source_warehouse": d.source_warehouse,
'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'), "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
'item_name': d.item_name, "item_name": d.item_name,
'description': d.description, "description": d.description,
'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
"rate": d.rate,
"amount": d.amount
}) })
def on_submit(self): def on_submit(self):
self.validate_transfer_qty()
self.validate_job_card() self.validate_job_card()
self.update_work_order() self.update_work_order()
self.set_transferred_qty() self.set_transferred_qty()
@@ -199,7 +311,16 @@ class JobCard(Document):
self.update_work_order() self.update_work_order()
self.set_transferred_qty() 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): 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: if not self.time_logs:
frappe.throw(_("Time logs are required for {0} {1}") frappe.throw(_("Time logs are required for {0} {1}")
.format(bold("Job Card"), get_link_to_form("Job Card", self.name))) .format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
@@ -215,6 +336,10 @@ class JobCard(Document):
if not self.work_order: if not self.work_order:
return 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 for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], [] from_time_list, to_time_list = [], []
@@ -225,10 +350,24 @@ class JobCard(Document):
time_in_mins = flt(data[0].time_in_mins) time_in_mins = flt(data[0].time_in_mins)
wo = frappe.get_doc('Work Order', self.work_order) 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.validate_produced_quantity(for_quantity, wo)
self.update_work_order_data(for_quantity, time_in_mins, 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): def validate_produced_quantity(self, for_quantity, wo):
if self.docstatus < 2: return if self.docstatus < 2: return
@@ -248,8 +387,8 @@ class JobCard(Document):
min(from_time) as start_time, max(to_time) as end_time min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE WHERE
jctl.parent = jc.name and jc.work_order = %s jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
and jc.operation_id = %s and jc.docstatus = 1 and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
""", (self.work_order, self.operation_id), as_dict=1) """, (self.work_order, self.operation_id), as_dict=1)
for data in wo.operations: for data in wo.operations:
@@ -271,7 +410,8 @@ class JobCard(Document):
def get_current_operation_data(self): def get_current_operation_data(self):
return frappe.get_all('Job Card', return frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], 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): def set_transferred_qty_in_job_card(self, ste_doc):
for row in ste_doc.items: for row in ste_doc.items:
@@ -354,7 +494,11 @@ class JobCard(Document):
.format(bold(self.operation), work_order), OperationMismatchError) .format(bold(self.operation), work_order), OperationMismatchError)
def validate_sequence_id(self): 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 current_operation_qty = 0.0
data = self.get_current_operation_data() data = self.get_current_operation_data()
@@ -376,6 +520,17 @@ class JobCard(Document):
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) .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() @frappe.whitelist()
def get_operation_details(work_order, operation): def get_operation_details(work_order, operation):
if work_order and operation: if work_order and operation:
@@ -511,3 +666,28 @@ def get_job_details(start, end, filters=None):
events.append(job_card_data) events.append(job_card_data)
return events 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

View File

@@ -25,8 +25,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Item Code", "label": "Item Code",
"options": "Item", "options": "Item"
"read_only": 1
}, },
{ {
"fieldname": "source_warehouse", "fieldname": "source_warehouse",
@@ -67,8 +66,7 @@
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Required Qty", "label": "Required Qty"
"read_only": 1
}, },
{ {
"fieldname": "column_break_9", "fieldname": "column_break_9",
@@ -107,7 +105,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-11 13:50:13.804108", "modified": "2021-04-22 18:50:00.003444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Item", "name": "Job Card Item",

View File

@@ -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
}

View 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 JobCardOperation(Document):
pass

View File

@@ -1,14 +1,17 @@
{ {
"actions": [],
"creation": "2019-03-08 23:56:43.187569", "creation": "2019-03-08 23:56:43.187569",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"employee",
"from_time", "from_time",
"to_time", "to_time",
"column_break_2", "column_break_2",
"time_in_mins", "time_in_mins",
"completed_qty" "completed_qty",
"operation"
], ],
"fields": [ "fields": [
{ {
@@ -41,10 +44,27 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Completed Qty", "label": "Completed Qty",
"reqd": 1 "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, "istable": 1,
"modified": "2019-12-03 12:56:02.285448", "links": [],
"modified": "2020-12-23 14:30:00.970916",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Time Log", "name": "Job Card Time Log",

View File

@@ -26,7 +26,10 @@
"column_break_16", "column_break_16",
"overproduction_percentage_for_work_order", "overproduction_percentage_for_work_order",
"other_settings_section", "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": [ "fields": [
{ {
@@ -155,13 +158,30 @@
{ {
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break" "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", "icon": "icon-wrench",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-10-13 10:55:43.996581", "modified": "2021-03-16 15:54:38.967341",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing Settings", "name": "Manufacturing Settings",

View File

@@ -2,7 +2,13 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Operation', { 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]]
}
};
});
} }
}); });

View File

@@ -1,167 +1,132 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "Prompt", "autoname": "Prompt",
"beta": 0,
"creation": "2014-11-07 16:20:30.683186", "creation": "2014-11-07 16:20:30.683186",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB", "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": [ "fields": [
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "workstation", "fieldname": "workstation",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Default Workstation", "label": "Default Workstation",
"length": 0, "options": "Workstation"
"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
}, },
{ {
"allow_on_submit": 0, "collapsible": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Operation Description"
"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
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text", "fieldtype": "Text",
"hidden": 0, "label": "Description"
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0, {
"in_filter": 0, "collapsible": 1,
"in_list_view": 0, "fieldname": "sub_operations_section",
"in_standard_filter": 0, "fieldtype": "Section Break",
"label": "Description", "label": "Sub Operations"
"length": 0, },
"no_copy": 0, {
"permlevel": 0, "fieldname": "sub_operations",
"precision": "", "fieldtype": "Table",
"print_hide": 0, "options": "Sub Operation"
"print_hide_if_no_value": 0, },
"read_only": 0, {
"remember_last_selected_value": 0, "description": "Time in mins.",
"report_hide": 0, "fieldname": "total_operation_time",
"reqd": 0, "fieldtype": "Float",
"search_index": 0, "label": "Total Operation Time",
"set_only_once": 0, "read_only": 1
"unique": 0 },
{
"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", "icon": "fa fa-wrench",
"idx": 0, "index_web_pages_for_search": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2021-01-12 15:09:23.593338",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-11-07 05:28:27.462413",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Operation", "name": "Operation",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 0,
"export": 1, "export": 1,
"if_owner": 0,
"import": 1, "import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0,
"role": "Manufacturing User", "role": "Manufacturing User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 0,
"export": 1, "export": 1,
"if_owner": 0,
"import": 1, "import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Manufacturing Manager", "role": "Manufacturing Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_seen": 0 "track_changes": 1
} }

View File

@@ -2,9 +2,34 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
class Operation(Document): class Operation(Document):
def validate(self): def validate(self):
if not self.description: if not self.description:
self.description = self.name 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

View File

@@ -306,8 +306,25 @@ frappe.ui.form.on('Production Plan', {
}, },
download_materials_required: function(frm) { download_materials_required: function(frm) {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; const fields = [{
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); fieldname: 'warehouses',
fieldtype: 'Table MultiSelect',
label: __('Warehouses'),
default: frm.doc.from_warehouse,
options: "Production Plan Material Request Warehouse",
get_query: function () {
return {
filters: {
company: frm.doc.company
}
};
},
}];
frappe.prompt(fields, (row) => {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses });
}, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock'));
}, },
show_progress: function(frm) { show_progress: function(frm) {

View File

@@ -477,18 +477,19 @@ class ProductionPlan(Document):
msgprint(_("No material request created")) msgprint(_("No material request created"))
@frappe.whitelist() @frappe.whitelist()
def download_raw_materials(doc): def download_raw_materials(doc, warehouses=None):
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production', 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty',
'Safety Stock', 'Required Qty']] 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']]
for d in get_items_for_material_requests(doc): doc.warehouse = None
for d in get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True):
item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
if not doc.get('for_warehouse'): if not doc.get('for_warehouse'):
row = {'item_code': d.get('item_code')} row = {'item_code': d.get('item_code')}
@@ -507,7 +508,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
item.purchase_uom, item_uom.conversion_factor item.purchase_uom, item_uom.conversion_factor, item.safety_stock
from from
`tabBOM Explosion Item` bei `tabBOM Explosion Item` bei
JOIN `tabBOM` bom ON bom.name = bei.parent JOIN `tabBOM` bom ON bom.name = bei.parent
@@ -677,32 +678,36 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin` ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
where item_code = %(item_code)s {conditions} ifnull(sum(planned_qty),0) as planned_qty
from `tabBin` where item_code = %(item_code)s {conditions}
group by item_code, warehouse group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
def get_warehouse_list(warehouses, warehouse_list=[]):
if isinstance(warehouses, string_types):
warehouses = json.loads(warehouses)
for row in warehouses:
child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
if child_warehouses:
warehouse_list.extend(child_warehouses)
else:
warehouse_list.append(row.get("warehouse"))
@frappe.whitelist() @frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None): def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
warehouse_list = [] warehouse_list = []
if warehouses: if warehouses:
if isinstance(warehouses, string_types): get_warehouse_list(warehouses, warehouse_list)
warehouses = json.loads(warehouses)
for row in warehouses:
child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
if child_warehouses:
warehouse_list.extend(child_warehouses)
else:
warehouse_list.append(row.get("warehouse"))
if warehouse_list: if warehouse_list:
warehouses = list(set(warehouse_list)) warehouses = list(set(warehouse_list))
if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses: if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses:
warehouses.remove(doc.get("for_warehouse")) warehouses.remove(doc.get("for_warehouse"))
warehouse_list = None warehouse_list = None
@@ -795,7 +800,7 @@ def get_items_for_material_requests(doc, warehouses=None):
if items: if items:
mr_items.append(items) mr_items.append(items)
if not ignore_existing_ordered_qty and warehouses: if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
new_mr_items = [] new_mr_items = []
for item in mr_items: for item in mr_items:
get_materials_from_other_locations(item, warehouses, new_mr_items, company) get_materials_from_other_locations(item, warehouses, new_mr_items, company)

View File

@@ -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) {
// }
});

View File

@@ -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
}

View 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

View File

@@ -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

View File

@@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase):
ste.submit() ste.submit()
stock_entries.append(ste) 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)) self.assertEqual(len(job_cards), len(bom.operations))
for i, job_card in enumerate(job_cards): for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card) doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", { doc.time_logs[0].completed_qty = 1
"from_time": add_to_date(None, i),
"hours": 1,
"to_time": add_to_date(None, i + 1),
"completed_qty": doc.for_quantity
})
doc.submit() doc.submit()
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))

View File

@@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", {
} }
if (frm.doc.docstatus === 1 if (frm.doc.docstatus === 1
&& frm.doc.operations && frm.doc.operations.length && frm.doc.operations && frm.doc.operations.length) {
&& frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
const not_completed = frm.doc.operations.filter(d => { const not_completed = frm.doc.operations.filter(d => {
if(d.status != 'Completed') { if(d.status != 'Completed') {
@@ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", {
const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
fields: [ fields: [
{ {
fieldtype:'Link', fieldtype: 'Link',
fieldname:'operation', fieldname: 'operation',
label: __('Operation'), label: __('Operation'),
read_only:1, read_only: 1,
in_list_view:1 in_list_view: 1
}, },
{ {
fieldtype:'Link', fieldtype: 'Link',
fieldname:'workstation', fieldname: 'workstation',
label: __('Workstation'), label: __('Workstation'),
read_only:1, read_only: 1,
in_list_view:1 in_list_view: 1
}, },
{ {
fieldtype:'Data', fieldtype: 'Data',
fieldname:'name', fieldname: 'name',
label: __('Operation Id') label: __('Operation Id')
}, },
{ {
fieldtype:'Float', fieldtype: 'Float',
fieldname:'pending_qty', fieldname: 'pending_qty',
label: __('Pending Qty'), label: __('Pending Qty'),
}, },
{ {
fieldtype:'Float', fieldtype: 'Float',
fieldname:'qty', fieldname: 'qty',
label: __('Quantity to Manufacture'), label: __('Quantity to Manufacture'),
read_only:0, read_only: 0,
in_list_view:1, in_list_view: 1,
},
{
fieldtype: 'Float',
fieldname: 'batch_size',
label: __('Batch Size'),
read_only: 1
}, },
], ],
data: operations_data, data: operations_data,
@@ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", {
}, function(data) { }, function(data) {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
freeze: true,
args: { args: {
work_order: frm.doc.name, work_order: frm.doc.name,
operations: data.operations, operations: data.operations,
},
callback: function() {
frm.reload_doc();
} }
}); });
}, __("Job Card"), __("Create")); }, __("Job Card"), __("Create"));
@@ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", {
if(data.completed_qty != frm.doc.qty) { if(data.completed_qty != frm.doc.qty) {
pending_qty = frm.doc.qty - flt(data.completed_qty); pending_qty = frm.doc.qty - flt(data.completed_qty);
dialog.fields_dict.operations.df.data.push({ if (pending_qty) {
'name': data.name, dialog.fields_dict.operations.df.data.push({
'operation': data.operation, 'name': data.name,
'workstation': data.workstation, 'operation': data.operation,
'qty': pending_qty, 'workstation': data.workstation,
'pending_qty': pending_qty, 'batch_size': data.batch_size,
}); 'qty': pending_qty,
'pending_qty': pending_qty
});
}
} }
}); });
dialog.fields_dict.operations.grid.refresh(); dialog.fields_dict.operations.grid.refresh();

View File

@@ -21,6 +21,12 @@
"produced_qty", "produced_qty",
"sales_order", "sales_order",
"project", "project",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_17",
"serial_no",
"batch_size",
"settings_section", "settings_section",
"allow_alternative_item", "allow_alternative_item",
"use_multi_level_bom", "use_multi_level_bom",
@@ -52,6 +58,7 @@
"actual_operating_cost", "actual_operating_cost",
"additional_operating_cost", "additional_operating_cost",
"column_break_24", "column_break_24",
"corrective_operation_cost",
"total_operating_cost", "total_operating_cost",
"more_info", "more_info",
"description", "description",
@@ -488,6 +495,57 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Lead Time", "label": "Lead Time",
"read_only": 1 "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", "icon": "fa fa-cogs",
@@ -495,7 +553,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-16 13:27:51.116484", "modified": "2021-06-20 15:19:14.902699",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@@ -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.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
from erpnext.utilities.transaction_base import validate_uom_is_integer from erpnext.utilities.transaction_base import validate_uom_is_integer
from frappe.model.mapper import get_mapped_doc 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 OverProductionError(frappe.ValidationError): pass
class CapacityError(frappe.ValidationError): pass class CapacityError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass
class ItemHasVariantError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass
class SerialNoQtyError(frappe.ValidationError):
from six import string_types pass
class WorkOrder(Document): class WorkOrder(Document):
@@ -123,7 +125,9 @@ class WorkOrder(Document):
variable_cost = self.actual_operating_cost if self.actual_operating_cost \ variable_cost = self.actual_operating_cost if self.actual_operating_cost \
else self.planned_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): def validate_work_order_against_so(self):
# already ordered qty # already ordered qty
@@ -231,6 +235,9 @@ class WorkOrder(Document):
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) 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): def on_submit(self):
if not self.wip_warehouse: if not self.wip_warehouse:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
@@ -262,6 +269,70 @@ class WorkOrder(Document):
self.update_planned_qty() self.update_planned_qty()
self.update_ordered_qty() self.update_ordered_qty()
self.update_reserved_qty_for_production() 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): def create_job_card(self):
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") 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) enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
for i, row in enumerate(self.operations): for index, row in enumerate(self.operations):
self.set_operation_start_end_time(i, row) qty = self.qty
while qty > 0:
if not row.workstation: qty = split_qty_based_on_batch_size(self, row, qty)
frappe.throw(_("Row {0}: select the workstation against the operation {1}") if row.job_card_qty > 0:
.format(row.idx, row.operation)) self.prepare_data_for_job_card(row, index,
plan_days, enable_capacity_planning)
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()
planned_end_date = self.operations and self.operations[-1].planned_end_time planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date: if planned_end_date:
self.db_set("planned_end_date", 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): def set_operation_start_end_time(self, idx, row):
"""Set start and end time for given operation. If first operation, set start as """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.""" `planned_start_date`, else add time diff to end time of earlier operation."""
@@ -666,6 +745,17 @@ class WorkOrder(Document):
bom.set_bom_material_details() bom.set_bom_material_details()
return bom 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.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): 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 return wo_doc
def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): 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) variant_items = json.loads(variant_items)
for item in 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.set_stock_entry_type()
stock_entry.get_items() stock_entry.get_items()
stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict() return stock_entry.as_dict()
@frappe.whitelist() @frappe.whitelist()
@@ -864,13 +955,47 @@ def query_sales_order(production_item):
@frappe.whitelist() @frappe.whitelist()
def make_job_card(work_order, operations): def make_job_card(work_order, operations):
if isinstance(operations, string_types): if isinstance(operations, str):
operations = json.loads(operations) operations = json.loads(operations)
work_order = frappe.get_doc('Work Order', work_order) work_order = frappe.get_doc('Work Order', work_order)
for row in operations: for row in operations:
row = frappe._dict(row)
validate_operation_data(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): def validate_operation_data(row):
if row.get("qty") <= 0: 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 = frappe.new_doc("Job Card")
doc.update({ doc.update({
'work_order': work_order.name, 'work_order': work_order.name,
'operation': row.get("operation"), 'operation': row.get("operation"),
'workstation': row.get("workstation"), 'workstation': row.get("workstation"),
'posting_date': nowdate(), '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"), 'operation_id': row.get("name"),
'bom_no': work_order.bom_no, 'bom_no': work_order.bom_no,
'project': work_order.project, 'project': work_order.project,
'company': work_order.company, 'company': work_order.company,
'sequence_id': row.get("sequence_id"), '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: if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer:

View File

@@ -4,10 +4,17 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'work_order', 'fieldname': 'work_order',
'non_standard_fieldnames': {
'Batch': 'reference_name'
},
'transactions': [ 'transactions': [
{ {
'label': _('Transactions'), 'label': _('Transactions'),
'items': ['Stock Entry', 'Job Card', 'Pick List'] 'items': ['Stock Entry', 'Job Card', 'Pick List']
},
{
'label': _('Reference'),
'items': ['Serial No', 'Batch']
} }
] ]
} }

View File

@@ -7,8 +7,9 @@
"details", "details",
"operation", "operation",
"bom", "bom",
"sequence_id", "column_break_4",
"description", "description",
"sequence_id",
"col_break1", "col_break1",
"completed_qty", "completed_qty",
"status", "status",
@@ -198,6 +199,10 @@
"label": "Sequence ID", "label": "Sequence ID",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,

View File

@@ -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
}
}
}
},
]
};

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]

View File

@@ -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.set_training_event_attendance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details

View 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
""")

View File

@@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', {
}); });
frm.set_query('employee', 'employees', () => { frm.set_query('employee', 'employees', () => {
if (!frm.doc.company) { let error_fields = [];
frappe.msgprint(__("Please set a Company")); let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
return [];
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 { return {
query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query", query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query",
filters: frm.events.get_employee_filters(frm) filters: frm.events.get_employee_filters(frm)
@@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', {
get_employee_filters: function (frm) { get_employee_filters: function (frm) {
let filters = {}; 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['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) { let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account',
filters['department'] = frm.doc.department; 'currency', 'department', 'branch', 'designation'];
}
if (frm.doc.branch) { fields.forEach(field => {
filters['branch'] = frm.doc.branch; if (frm.doc[field]) {
} filters[field] = frm.doc[field];
if (frm.doc.designation) { }
filters['designation'] = frm.doc.designation; });
}
if (frm.doc.employees) { 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; return filters;
}, },

View File

@@ -459,6 +459,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account):
where where
t1.name = t2.employee t1.name = t2.employee
and t2.docstatus = 1 and t2.docstatus = 1
and t1.status != 'Inactive'
%s order by t2.from_date desc %s order by t2.from_date desc
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
@@ -679,6 +680,10 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
conditions = [] conditions = []
include_employees = [] include_employees = []
emp_cond = '' emp_cond = ''
if not filters.payroll_frequency:
frappe.throw(_('Select Payroll Frequency.'))
if filters.start_date and filters.end_date: if filters.start_date and filters.end_date:
employee_list = get_employee_list(filters) employee_list = get_employee_list(filters)
emp = filters.get('employees') emp = filters.get('employees')

View File

@@ -4,6 +4,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import get_link_to_form
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@@ -18,6 +20,27 @@ class ProductBundle(Document):
from erpnext.utilities.transaction_base import validate_uom_is_integer from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "uom", "qty") 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): def validate_main_item(self):
"""Validates, main Item is not a stock item""" """Validates, main Item is not a stock item"""
if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"): if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "field:batch_id", "autoname": "field:batch_id",
"creation": "2013-03-05 14:50:38", "creation": "2013-03-05 14:50:38",
@@ -25,7 +26,11 @@
"reference_doctype", "reference_doctype",
"reference_name", "reference_name",
"section_break_7", "section_break_7",
"description" "description",
"manufacturing_section",
"qty_to_produce",
"column_break_23",
"produced_qty"
], ],
"fields": [ "fields": [
{ {
@@ -160,13 +165,35 @@
"label": "Batch UOM", "label": "Batch UOM",
"options": "UOM", "options": "UOM",
"read_only": 1 "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", "icon": "fa fa-archive",
"idx": 1, "idx": 1,
"image_field": "image", "image_field": "image",
"links": [],
"max_attachments": 5, "max_attachments": 5,
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-01-07 11:10:09.149170",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Batch", "name": "Batch",

View File

@@ -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" message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
frappe.throw(_("There is no batch found against the {0}: {1}") frappe.throw(_("There is no batch found against the {0}: {1}")
.format(message, serial_no_link)) .format(message, serial_no_link))
def make_batch(args):
if frappe.db.get_value("Item", args.item, "has_batch_no"):
args.doctype = "Batch"
frappe.get_doc(args).insert().name

View File

@@ -254,6 +254,8 @@ class PurchaseReceipt(BuyingController):
return process_gl_map(gl_entries) return process_gl_map(gl_entries)
def make_item_gl_entries(self, gl_entries, warehouse_account=None): def make_item_gl_entries(self, gl_entries, warehouse_account=None):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_purchase_document_details
stock_rbnb = self.get_company_default("stock_received_but_not_billed") stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name) landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
@@ -262,6 +264,8 @@ class PurchaseReceipt(BuyingController):
warehouse_with_no_account = [] warehouse_with_no_account = []
stock_items = self.get_stock_items() stock_items = self.get_stock_items()
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
for d in self.get("items"): for d in self.get("items"):
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse): if warehouse_account.get(d.warehouse):
@@ -304,6 +308,23 @@ class PurchaseReceipt(BuyingController):
-1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name, -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name,
debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d) debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d)
# check if the exchange rate has changed
if d.get('purchase_invoice'):
if exchange_rate_map[d.purchase_invoice] and \
self.conversion_rate != exchange_rate_map[d.purchase_invoice] and \
d.net_rate == net_rate_map[d.purchase_invoice_item]:
discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * \
(exchange_rate_map[d.purchase_invoice] - self.conversion_rate)
self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference,
remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency, item=d)
self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency, item=d)
# Amount added through landed-cos-voucher # Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries: if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):

View File

@@ -1052,6 +1052,33 @@ class TestPurchaseReceipt(unittest.TestCase):
frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value)
def test_purchase_receipt_with_exchange_rate_difference(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice
pi = create_purchase_invoice(currency = "USD", conversion_rate = 70)
create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
properties={"account": '_Test Account Stock In Hand - TCP1'})
pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80,
do_not_save = "True")
pr.items[0].purchase_invoice = pi.name
pr.items[0].purchase_invoice_item = pi.items[0].name
pr.insert()
pr.submit()
# fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account
gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'})
voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
self.assertEqual(pr.name, voucher_no)
exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
def get_sl_entries(voucher_type, voucher_no): def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s

View File

@@ -57,7 +57,8 @@
"more_info", "more_info",
"serial_no_details", "serial_no_details",
"company", "company",
"status" "status",
"work_order"
], ],
"fields": [ "fields": [
{ {
@@ -422,12 +423,18 @@
"label": "Status", "label": "Status",
"options": "\nActive\nInactive\nDelivered\nExpired", "options": "\nActive\nInactive\nDelivered\nExpired",
"read_only": 1 "read_only": 1
},
{
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work Order"
} }
], ],
"icon": "fa fa-barcode", "icon": "fa fa-barcode",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2020-07-20 20:50:16.660433", "modified": "2021-01-08 14:31:15.375996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Serial No", "name": "Serial No",

View File

@@ -473,16 +473,13 @@ def get_serial_nos(serial_no):
if s.strip()] if s.strip()]
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
serial_no_doc.update({ for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
"item_code": args.get("item_code"), if args.get(field):
"company": args.get("company"), serial_no_doc.set(field, args.get(field))
"batch_no": args.get("batch_no"),
"via_stock_ledger": args.get("via_stock_ledger") or True, serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
"supplier": args.get("supplier"), serial_no_doc.warehouse = (args.get("warehouse")
"location": args.get("location"), if args.get("actual_qty", 0) > 0 else None)
"warehouse": (args.get("warehouse")
if args.get("actual_qty", 0) > 0 else None)
})
if is_new: if is_new:
serial_no_doc.serial_no = serial_no serial_no_doc.serial_no = serial_no

View File

@@ -498,6 +498,7 @@ class StockEntry(StockController):
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse: if not d.t_warehouse:
outgoing_items_cost += flt(d.basic_amount) outgoing_items_cost += flt(d.basic_amount)
return outgoing_items_cost return outgoing_items_cost
def get_args_for_incoming_rate(self, item): def get_args_for_incoming_rate(self, item):
@@ -854,6 +855,7 @@ class StockEntry(StockController):
pro_doc.run_method("update_work_order_qty") pro_doc.run_method("update_work_order_qty")
if self.purpose == "Manufacture": if self.purpose == "Manufacture":
pro_doc.run_method("update_planned_qty") pro_doc.run_method("update_planned_qty")
pro_doc.update_batch_produced_qty(self)
if not pro_doc.operations: if not pro_doc.operations:
pro_doc.set_actual_dates() pro_doc.set_actual_dates()
@@ -1076,18 +1078,54 @@ class StockEntry(StockController):
# in case of BOM # in case of BOM
to_warehouse = item.get("default_warehouse") 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({ self.add_to_stock_entry_detail({
item.name: { item.name: 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
}
}, bom_no = self.bom_no) }, bom_no = self.bom_no)
def get_bom_raw_materials(self, qty): def get_bom_raw_materials(self, qty):
@@ -1524,6 +1562,36 @@ class StockEntry(StockController):
material_requests.append(material_request) material_requests.append(material_request)
frappe.db.set_value('Material Request', material_request, 'transfer_status', status) 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() @frappe.whitelist()
def move_sample_to_retention_warehouse(company, items): def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types): if isinstance(items, string_types):
@@ -1635,6 +1703,10 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
if bom.quantity: if bom.quantity:
operating_cost_per_unit = flt(bom.operating_cost) / flt(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 return operating_cost_per_unit
def get_used_alternative_items(purchase_order=None, work_order=None): def get_used_alternative_items(purchase_order=None, work_order=None):

View File

@@ -18,6 +18,7 @@
"col_break2", "col_break2",
"is_finished_item", "is_finished_item",
"is_scrap_item", "is_scrap_item",
"quality_inspection",
"subcontracted_item", "subcontracted_item",
"section_break_8", "section_break_8",
"description", "description",
@@ -69,7 +70,6 @@
"putaway_rule", "putaway_rule",
"column_break_51", "column_break_51",
"reference_purchase_receipt", "reference_purchase_receipt",
"quality_inspection",
"job_card_item" "job_card_item"
], ],
"fields": [ "fields": [
@@ -548,7 +548,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-11 13:47:50.158754", "modified": "2021-04-22 20:08:23.799715",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",

View File

@@ -48,37 +48,54 @@ frappe.ui.form.on("Stock Reconciliation", {
}, },
get_items: function(frm) { 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() { "get_query": function() {
return { return {
"filters": { "filters": {
"company": frm.doc.company, "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) { posting_date: function(frm) {

View File

@@ -481,45 +481,99 @@ class StockReconciliation(StockController):
self._cancel() self._cancel()
@frappe.whitelist() @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"]) lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
items = frappe.db.sql(""" 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 from tabBin bin, tabItem i
where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1 where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1
and i.has_variants = 0 and i.has_batch_no = 0 and i.has_variants = 0 and exists(
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse) select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse
""", (lft, rgt)) )
""", (lft, rgt), as_dict=1)
items += frappe.db.sql(""" 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 from tabItem i, `tabItem Default` id
where i.name = id.parent where i.name = id.parent
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse) 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.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s
and i.has_variants = 0 and i.disabled = 0 and id.company=%s
group by i.name group by i.name
""", (lft, rgt, company)) """, (lft, rgt, company), as_dict=1)
res = [] return items
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]))
if frappe.db.get_value("Item", d[0], "disabled") == 0: def get_item_data(row, qty, valuation_rate, serial_no=None):
res.append({ return {
"item_code": d[0], 'item_code': row.item_code,
"warehouse": d[2], 'warehouse': row.warehouse,
"qty": stock_bal[0], 'qty': qty,
"item_name": d[1], 'item_name': row.item_name,
"valuation_rate": stock_bal[1], 'valuation_rate': valuation_rate,
"current_qty": stock_bal[0], 'current_qty': qty,
"current_valuation_rate": stock_bal[1], 'current_valuation_rate': valuation_rate,
"current_serial_no": stock_bal[2] if cint(d[3]) else '', 'current_serial_no': serial_no,
"serial_no": stock_bal[2] if cint(d[3]) else '' '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() @frappe.whitelist()
def get_stock_balance_for(item_code, warehouse, def get_stock_balance_for(item_code, warehouse,

View File

@@ -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'
}
]
};

View File

@@ -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"
}
]
}

View File

@@ -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
}]

View File

@@ -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")
}
]
};

View File

@@ -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"
}
]
}

View File

@@ -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
}]

View File

@@ -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"
}
]
};

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]

View File

@@ -15,6 +15,7 @@
"hide_custom": 0, "hide_custom": 0,
"icon": "stock", "icon": "stock",
"idx": 0, "idx": 0,
"is_default": 0,
"is_standard": 1, "is_standard": 1,
"label": "Stock", "label": "Stock",
"links": [ "links": [
@@ -653,9 +654,44 @@
"link_type": "Report", "link_type": "Report",
"onboard": 0, "onboard": 0,
"type": "Link" "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", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock", "name": "Stock",

View File

@@ -166,7 +166,7 @@
"options": "Service Level Agreement" "options": "Service Level Agreement"
}, },
{ {
"depends_on": "eval: doc.status != 'Replied';", "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
"fieldname": "response_by", "fieldname": "response_by",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Response By", "label": "Response By",
@@ -180,7 +180,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: doc.status != 'Replied';", "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
"fieldname": "resolution_by", "fieldname": "resolution_by",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Resolution By", "label": "Resolution By",
@@ -410,7 +410,7 @@
"icon": "fa fa-ticket", "icon": "fa fa-ticket",
"idx": 7, "idx": 7,
"links": [], "links": [],
"modified": "2021-05-26 10:49:07.574769", "modified": "2021-06-10 03:22:27.098898",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "Issue", "name": "Issue",

View File

@@ -26,6 +26,9 @@ class Issue(Document):
self.set_lead_contact(self.raised_by) self.set_lead_contact(self.raised_by)
if not self.service_level_agreement:
self.reset_sla_fields()
def on_update(self): def on_update(self):
# Add a communication in the issue timeline # Add a communication in the issue timeline
if self.flags.create_communication and self.via_customer_portal: 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 \ self.company = frappe.db.get_value("Lead", self.lead, "company") or \
frappe.db.get_default("Company") 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): def create_communication(self):
communication = frappe.new_doc("Communication") communication = frappe.new_doc("Communication")
communication.update({ communication.update({

View File

@@ -142,7 +142,7 @@ def link_existing_conversations(doc, state):
for log in logs: for log in logs:
call_log = frappe.get_doc('Call Log', log) call_log = frappe.get_doc('Call Log', log)
call_log.add_link(link_type=doc.doctype, link_name=doc.name) call_log.add_link(link_type=doc.doctype, link_name=doc.name)
call_log.save() call_log.save(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()
except Exception: except Exception:
frappe.log_error(title=_('Error during caller information update')) frappe.log_error(title=_('Error during caller information update'))

View File

@@ -1,28 +1,54 @@
{% if doc.status=="Open" %} {% if doc.status == "Open" %}
<div class="web-list-item"> <div class="web-list-item transaction-list-item">
<a class="no-decoration" href="/projects?project={{ doc.name | urlencode }}"> <div class="row">
<div class="row"> <div class="col-xs-2">
<div class="col-xs-6"> <a class="transaction-item-link" href="/projects?project={{ doc.name | urlencode }}">Link</a>
{{ doc.name }}
{{ doc.name }} </div>
</div> <div class="col-xs-2">
<div class="col-xs-3"> {{ doc.project_name }}
{% if doc.percent_complete %} </div>
<div class="progress" style="margin-bottom: 0!important; margin-top: 10px!important; height:5px;"> <div class="col-xs-3 text-center">
<div class="progress-bar progress-bar-{{ "warning" if doc.percent_complete|round < 100 else "success"}}" role="progressbar" {% if doc.percent_complete %}
aria-valuenow="{{ doc.percent_complete|round|int }}" {% set pill_class = "green" if doc.percent_complete | round == 100 else
aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;"> "orange" %}
</div> <div class="ellipsis">
</div> <span class="indicator-pill {{ pill_class }} filterable ellipsis">
{% else %} <span>{{ frappe.utils.cint(doc.percent_complete) }}
<span class="indicator {{ "red" if doc.status=="Open" else "gray" }}"> %</span>
{{ doc.status }}</span> </span>
{% endif %} </div>
</div> {% else %}
<div class="col-xs-3 text-right small text-muted"> <span class="indicator-pill {{ " red" if doc.status=="Open" else "darkgrey" }}">
{{ frappe.utils.pretty_date(doc.modified) }} {{ doc.status }}</span>
</div> {% endif %}
</div> </div>
</a> {% if doc["_assign"] %}
</div> {% set assigned_users = json.loads(doc["_assign"])%}
<div class="col-xs-2">
{% for user in assigned_users %}
{% set user_details = frappe
.db
.get_value("User", user, [
"full_name", "user_image"
], as_dict = True) %}
{% if user_details.user_image %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<img src="{{ user_details.user_image }}">
</span>
{% else %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<div class='standard-image' style="background-color: #F5F4F4; color: #000;">
{{ frappe.utils.get_abbr(user_details.full_name) }}
</div>
</span>
{% endif %}
{% endfor %}
</div>
{% endif %}
<div class="col-xs-3 text-right small text-muted">
{{ frappe.utils.pretty_date(doc.modified) }}
</div>
</div>
</div>
{% endif %} {% endif %}

View File

@@ -1,32 +1,5 @@
{% for task in doc.tasks %} {% for task in doc.tasks %}
<div class='task'> <div class="web-list-item transaction-list-item">
<a class="no-decoration task-link {{ task.css_seen }}" href="/tasks?name={{ task.name }}"> {{ task_row(task, 0) }}
<div class='row project-item'> </div>
<div class='col-xs-9'>
<span class="indicator {{ "red" if task.status=="Open" else "green" if task.status=="Closed" else "gray" }}" title="{{ task.status }}" > {{ task.subject }}</span>
<div class="small text-muted item-timestamp"
title="{{ frappe.utils.pretty_date(task.modified) }}">
{{ _("modified") }} {{ frappe.utils.pretty_date(task.modified) }}
</div>
</div>
<div class='col-xs-1'>{% if task.todo %}
{% if task.todo.user_image %}
<span class="avatar avatar-small" title="{{ task.todo.owner }}">
<img src="{{ task.todo.user_image }}">
</span>
{% else %}
<span class="avatar avatar-small standard-image" title="Assigned to {{ task.todo.owner }}">
</span>
{% endif %}
{% endif %} </div>
<div class='col-xs-2'>
<span class="pull-right list-comment-count small {{ "text-extra-muted" if task.comment_count==0 else "text-muted" }}">
<i class="octicon octicon-comment-discussion"></i>
{{ task.comment_count }}
</span>
</div>
</div>
</a>
</div>
{% endfor %} {% endfor %}

View File

@@ -1,23 +1,33 @@
{% for timesheet in doc.timesheets %} {% for timesheet in doc.timesheets %}
<div class='timesheet'> <div class="web-list-item transaction-list-item">
<a class="no-decoration timesheet-link {{ timesheet.css_seen }}" href="/timesheet/{{ timesheet.info.name}}"> <div class="row">
<div class='row project-item'> <div class="col-xs-2">{{ timesheet.name }}</div>
<div class='col-xs-10'> <a class="transaction-item-link" href="/timesheet/{{ timesheet.name}}">Link</a>
<span class="indicator {{ "blue" if timesheet.info.status=="Submitted" else "red" if timesheet.info.status=="Draft" else "gray" }}" title="{{ timesheet.info.status }}" > {{ timesheet.info.name }} </span> <div class="col-xs-2">{{ timesheet.status }}</div>
<div class="small text-muted item-timestamp"> <div class="col-xs-2">{{ frappe.utils.format_date(timesheet.from_time, "medium") }}</div>
{{ _("From") }} {{ frappe.format_date(timesheet.from_time) }} {{ _("to") }} {{ frappe.format_date(timesheet.to_time) }} <div class="col-xs-2">{{ frappe.utils.format_date(timesheet.to_time, "medium") }}</div>
</div> <div class="col-xs-2">
</div> {% set user_details = frappe
<div class='col-xs-1' style="margin-right:-30px;"> .db
<span class="avatar avatar-small" title="{{ timesheet.info.modified_by }}"> <img src="{{ timesheet.info.user_image }}" style="display:flex;"></span> .get_value("User", timesheet.modified_by, [
</div> "full_name", "user_image"
<div class='col-xs-1'> ], as_dict = True)
<span class="pull-right list-comment-count small {{ "text-extra-muted" if timesheet.comment_count==0 else "text-muted" }}"> %}
<i class="octicon octicon-comment-discussion"></i> {% if user_details.user_image %}
{{ timesheet.info.comment_count }} <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
</span> <img src="{{ user_details.user_image }}">
</div> </span>
</div> {% else %}
</a> <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
</div> <div class='standard-image' style='background-color: #F5F4F4; color: #000;'>
{{ frappe.utils.get_abbr(user_details.full_name) }}
</div>
</span>
{% endif %}
</div>
<div class="col-xs-2 text-right">
{{ frappe.utils.pretty_date(timesheet.modified) }}
</div>
</div>
</div>
{% endfor %} {% endfor %}

View File

@@ -1,90 +1,173 @@
{% extends "templates/web.html" %} {% extends "templates/web.html" %}
{% block title %}{{ doc.project_name }}{% endblock %} {% block title %}
{{ doc.project_name }}
{% endblock %}
{% block head_include %}
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block header %} {% block header %}
<h1>{{ doc.project_name }}</h1> <h1>{{ doc.project_name }}</h1>
{% endblock %} {% endblock %}
{% block style %} {% block style %}
<style> <style>
{% include "templates/includes/projects.css" %} {
</style> % include "templates/includes/projects.css"%
}
</style>
{% endblock %} {% endblock %}
{% block page_content %} {% block page_content %}
{% if doc.percent_complete %}
<div class="progress progress-hg">
<div class="progress-bar progress-bar-{{ "warning" if doc.percent_complete|round < 100 else "success" }} active" role="progressbar" aria-valuenow="{{ doc.percent_complete|round|int }}"
aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;">
</div>
</div>
{% endif %}
<div class="clearfix"> {{ progress_bar(doc.percent_complete) }}
<h4 style="float: left;">{{ _("Tasks") }}</h4>
<a class="btn btn-secondary btn-light btn-sm" style="float: right; position: relative; top: 10px;" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
</div>
<p> <div class="d-flex mt-5 mb-5 justify-content-between">
<!-- <a class='small underline task-status-switch' data-status='Open'>{{ _("Show closed") }}</a> --> <h4>Status:</h4>
</p> <h4>Progress:
<span>{{ doc.percent_complete }}
%</span>
</h4>
<h4>Hours Spent:
<span>{{ doc.actual_time }}</span>
</h4>
</div>
{% if doc.tasks %} {{ progress_bar(doc.percent_complete) }}
<div class='project-task-section'>
<div class='project-task'>
{% include "erpnext/templates/includes/projects/project_tasks.html" %}
</div>
<p><a id= 'more-task' style='display: none;' class='more-tasks small underline'>{{ _("More") }}</a><p>
</div>
{% else %}
<p class="text-muted">{{ _("No tasks") }}</p>
{% endif %}
{% if doc.tasks %}
<div class="website-list">
<div class="result">
<div class="web-list-item transaction-list-item">
<div class="row">
<h3 class="col-xs-4">Tasks</h3>
<h3 class="col-xs-2">Status</h3>
<h3 class="col-xs-2">End Date</h3>
<h3 class="col-xs-2">Assigned To</h3>
<div class="col-xs-2 text-right">
<a class="btn btn-secondary btn-light btn-sm" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
</div>
</div>
</div>
{% include "erpnext/templates/includes/projects/project_tasks.html" %}
</div>
</div>
{% else %}
<p class="font-weight-bold">{{ _("No Tasks") }}</p>
{% endif %}
<div class='padding'></div> {% if doc.timesheets %}
<div class="website-list">
<div class="result">
<div class="web-list-item transaction-list-item">
<div class="row">
<h3 class="col-xs-2">Timesheets</h3>
<h3 class="col-xs-2">Status</h3>
<h3 class="col-xs-2">From</h3>
<h3 class="col-xs-2">To</h3>
<h3 class="col-xs-2">Modified By</h3>
<h3 class="col-xs-2 text-right">Modified On</h3>
</div>
</div>
{% include "erpnext/templates/includes/projects/project_timesheets.html" %}
</div>
</div>
{% else %}
<p class="font-weight-bold mt-5">{{ _("No Timesheets") }}</p>
{% endif %}
<h4>{{ _("Timesheets") }}</h4> {% if doc.attachments %}
<div class='padding'></div>
{% if doc.timesheets %} <h4>{{ _("Attachments") }}</h4>
<div class='project-timelogs'> <div class="project-attachments">
{% include "erpnext/templates/includes/projects/project_timesheets.html" %} {% for attachment in doc.attachments %}
</div> <div class="attachment">
{% if doc.timesheets|length > 9 %} <a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank">
<p><a class='more-timelogs small underline'>{{ _("More") }}</a><p> <div class="row">
{% endif %} <div class="col-xs-9">
{% else %} <span class="indicator red file-name">
<p class="text-muted">{{ _("No time sheets") }}</p> {{ attachment.file_name }}</span>
{% endif %} </div>
<div class="col-xs-3">
{% if doc.attachments %} <span class="pull-right file-size">{{ attachment.file_size }}</span>
<div class='padding'></div> </div>
</div>
<h4>{{ _("Attachments") }}</h4> </a>
<div class="project-attachments"> </div>
{% for attachment in doc.attachments %} {% endfor %}
<div class="attachment"> </div>
<a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank"> {% endif %}
<div class="row">
<div class="col-xs-9">
<span class="indicator red file-name"> {{ attachment.file_name }}</span>
</div>
<div class="col-xs-3">
<span class="pull-right file-size">{{ attachment.file_size }}</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% endif %}
</div> </div>
<script> <script>
{% include "frappe/public/js/frappe/provide.js" %} { % include "frappe/public/js/frappe/provide.js" %
{% include "frappe/public/js/frappe/form/formatters.js" %} } { % include "frappe/public/js/frappe/form/formatters.js" %
}
</script> </script>
{% endblock %} {% endblock %}
{% macro progress_bar(percent_complete) %}
{% if percent_complete %}
<div class="progress progress-hg" style="height: 5px;">
<div class="progress-bar progress-bar-{{ 'warning' if percent_complete|round < 100 else 'success' }} active" role="progressbar" aria-valuenow="{{ percent_complete|round|int }}" aria-valuemin="0" aria-valuemax="100" style="width:{{ percent_complete|round|int }}%;"></div>
</div>
{% else %}
<hr>
{% endif %}
{% endmacro %}
{% macro task_row(task, indent) %}
<div class="row mt-5 {% if task.children %} font-weight-bold {% endif %}">
<div class="col-xs-4">
<a class="nav-link " style="color: inherit; {% if task.parent_task %} margin-left: {{ indent }}px {% endif %}" href="/tasks?name={{ task.name | urlencode }}">
{% if task.parent_task %}
<span class="">
<i class="fa fa-level-up fa-rotate-90"></i>
</span>
{% endif %}
{{ task.subject }}</a>
</div>
<div class="col-xs-2">{{ task.status }}</div>
<div class="col-xs-2">
{% if task.exp_end_date %}
{{ task.exp_end_date }}
{% else %}
--
{% endif %}
</div>
<div class="col-xs-2">
{% if task["_assign"] %}
{% set assigned_users = json.loads(task["_assign"])%}
{% for user in assigned_users %}
{% set user_details = frappe.db.get_value("User", user,
["full_name", "user_image"],
as_dict = True)%}
{% if user_details.user_image %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<img src="{{ user_details.user_image }}">
</span>
{% else %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<div class='standard-image' style='background-color: #F5F4F4; color: #000;'>
{{ frappe.utils.get_abbr(user_details.full_name) }}
</div>
</span>
{% endif %}
{% endfor %}
{% endif %}
</div>
<div class="col-xs-2 text-right">
{{ frappe.utils.pretty_date(task.modified) }}
</div>
</div>
{% if task.children %}
{% for child in task.children %}
{{ task_row(child, indent + 30) }}
{% endfor %}
{% endif %}
{% endmacro %}

View File

@@ -32,29 +32,17 @@ def get_tasks(project, start=0, search=None, item_status=None):
filters = {"project": project} filters = {"project": project}
if search: if search:
filters["subject"] = ("like", "%{0}%".format(search)) filters["subject"] = ("like", "%{0}%".format(search))
# if item_status:
# filters["status"] = item_status
tasks = frappe.get_all("Task", filters=filters, tasks = frappe.get_all("Task", filters=filters,
fields=["name", "subject", "status", "_seen", "_comments", "modified", "description"], fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"],
limit_start=start, limit_page_length=10) limit_start=start, limit_page_length=10)
task_nest = []
for task in tasks: for task in tasks:
task.todo = frappe.get_all('ToDo',filters={'reference_name':task.name, 'reference_type':'Task'}, if task.is_group:
fields=["assigned_by", "owner", "modified", "modified_by"]) child_tasks = list(filter(lambda x: x.parent_task == task.name, tasks))
if len(child_tasks):
if task.todo: task.children = child_tasks
task.todo=task.todo[0] task_nest.append(task)
task.todo.user_image = frappe.db.get_value('User', task.todo.owner, 'user_image') return list(filter(lambda x: not x.parent_task, tasks))
task.comment_count = len(json.loads(task._comments or "[]"))
task.css_seen = ''
if task._seen:
if frappe.session.user in json.loads(task._seen):
task.css_seen = 'seen'
return tasks
@frappe.whitelist() @frappe.whitelist()
def get_task_html(project, start=0, item_status=None): def get_task_html(project, start=0, item_status=None):
@@ -74,19 +62,11 @@ def get_timesheets(project, start=0, search=None):
fields=['project','activity_type','from_time','to_time','parent'], fields=['project','activity_type','from_time','to_time','parent'],
limit_start=start, limit_page_length=10) limit_start=start, limit_page_length=10)
for timesheet in timesheets: for timesheet in timesheets:
timesheet.infos = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, info = frappe.get_all('Timesheet', filters={"name": timesheet.parent},
fields=['name','_comments','_seen','status','modified','modified_by'], fields=['name','status','modified','modified_by'],
limit_start=start, limit_page_length=10) limit_start=start, limit_page_length=10)
if len(info):
for timesheet.info in timesheet.infos: timesheet.update(info[0])
timesheet.info.user_image = frappe.db.get_value('User', timesheet.info.modified_by, 'user_image')
timesheet.info.comment_count = len(json.loads(timesheet.info._comments or "[]"))
timesheet.info.css_seen = ''
if timesheet.info._seen:
if frappe.session.user in json.loads(timesheet.info._seen):
timesheet.info.css_seen = 'seen'
return timesheets return timesheets
@frappe.whitelist() @frappe.whitelist()