mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-07 07:02:54 +00:00
Merge branch 'develop' into print-language
This commit is contained in:
@@ -22,8 +22,7 @@ frappe.ui.form.on("Account", {
|
|||||||
// hide fields if group
|
// hide fields if group
|
||||||
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
||||||
|
|
||||||
// disable fields
|
frm.toggle_enable(["is_group", "company", "account_number"], frm.is_new());
|
||||||
frm.toggle_enable(["is_group", "company"], false);
|
|
||||||
|
|
||||||
if (cint(frm.doc.is_group) == 0) {
|
if (cint(frm.doc.is_group) == 0) {
|
||||||
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
|
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
|
||||||
|
|||||||
@@ -55,8 +55,7 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Account Number",
|
"label": "Account Number"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -74,7 +73,6 @@
|
|||||||
"oldfieldname": "company",
|
"oldfieldname": "company",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 1,
|
"remember_last_selected_value": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1525,7 +1525,8 @@
|
|||||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||||
"Clients cr\u00e9diteurs": {
|
"Clients cr\u00e9diteurs": {
|
||||||
"Clients - Avances et acomptes re\u00e7us sur commandes": {
|
"Clients - Avances et acomptes re\u00e7us sur commandes": {
|
||||||
"account_number": "4191"
|
"account_number": "4191",
|
||||||
|
"account_type": "Income Account"
|
||||||
},
|
},
|
||||||
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
|
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
|
||||||
"account_number": "4196"
|
"account_number": "4196"
|
||||||
@@ -3141,4 +3142,4 @@
|
|||||||
"account_number": "7"
|
"account_number": "7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ class AutoMatchbyAccountIBAN:
|
|||||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "bank_ac_no" in or_filters:
|
||||||
|
or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
|
||||||
|
|
||||||
if party_result:
|
if party_result:
|
||||||
result = (
|
result = (
|
||||||
party,
|
party,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_days, format_date, getdate
|
from frappe.utils import add_days, flt, format_date, getdate
|
||||||
|
|
||||||
|
|
||||||
class MainCostCenterCantBeChild(frappe.ValidationError):
|
class MainCostCenterCantBeChild(frappe.ValidationError):
|
||||||
@@ -60,7 +60,7 @@ class CostCenterAllocation(Document):
|
|||||||
self.validate_child_cost_centers()
|
self.validate_child_cost_centers()
|
||||||
|
|
||||||
def validate_total_allocation_percentage(self):
|
def validate_total_allocation_percentage(self):
|
||||||
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
total_percentage = sum([flt(d.percentage) for d in self.get("allocation_percentages", [])])
|
||||||
|
|
||||||
if total_percentage != 100:
|
if total_percentage != 100:
|
||||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ class JournalEntry(AccountsController):
|
|||||||
self.update_asset_value()
|
self.update_asset_value()
|
||||||
self.update_inter_company_jv()
|
self.update_inter_company_jv()
|
||||||
self.update_invoice_discounting()
|
self.update_invoice_discounting()
|
||||||
|
self.update_booked_depreciation()
|
||||||
|
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
if hasattr(self, "repost_required"):
|
if hasattr(self, "repost_required"):
|
||||||
@@ -225,6 +226,7 @@ class JournalEntry(AccountsController):
|
|||||||
self.unlink_inter_company_jv()
|
self.unlink_inter_company_jv()
|
||||||
self.unlink_asset_adjustment_entry()
|
self.unlink_asset_adjustment_entry()
|
||||||
self.update_invoice_discounting()
|
self.update_invoice_discounting()
|
||||||
|
self.update_booked_depreciation()
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.pay_to_recd_from or self.accounts[0].account
|
return self.pay_to_recd_from or self.accounts[0].account
|
||||||
@@ -442,6 +444,28 @@ class JournalEntry(AccountsController):
|
|||||||
if status:
|
if status:
|
||||||
inv_disc_doc.set_status(status=status)
|
inv_disc_doc.set_status(status=status)
|
||||||
|
|
||||||
|
def update_booked_depreciation(self):
|
||||||
|
for d in self.get("accounts"):
|
||||||
|
if (
|
||||||
|
self.voucher_type == "Depreciation Entry"
|
||||||
|
and d.reference_type == "Asset"
|
||||||
|
and d.reference_name
|
||||||
|
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||||
|
and d.debit
|
||||||
|
):
|
||||||
|
asset = frappe.get_doc("Asset", d.reference_name)
|
||||||
|
for fb_row in asset.get("finance_books"):
|
||||||
|
if fb_row.finance_book == self.finance_book:
|
||||||
|
depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book)
|
||||||
|
total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations
|
||||||
|
for je in depr_schedule:
|
||||||
|
if je.journal_entry:
|
||||||
|
total_number_of_booked_depreciations += 1
|
||||||
|
fb_row.db_set(
|
||||||
|
"total_number_of_booked_depreciations", total_number_of_booked_depreciations
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
def unlink_advance_entry_reference(self):
|
def unlink_advance_entry_reference(self):
|
||||||
for d in self.get("accounts"):
|
for d in self.get("accounts"):
|
||||||
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||||
@@ -1034,6 +1058,17 @@ class JournalEntry(AccountsController):
|
|||||||
|
|
||||||
def build_gl_map(self):
|
def build_gl_map(self):
|
||||||
gl_map = []
|
gl_map = []
|
||||||
|
|
||||||
|
company_currency = erpnext.get_company_currency(self.company)
|
||||||
|
if self.multi_currency:
|
||||||
|
for row in self.get("accounts"):
|
||||||
|
if row.account_currency != company_currency:
|
||||||
|
self.currency = row.account_currency
|
||||||
|
self.conversion_rate = row.exchange_rate
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.currency = company_currency
|
||||||
|
|
||||||
for d in self.get("accounts"):
|
for d in self.get("accounts"):
|
||||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||||
r = [d.user_remark, self.remark]
|
r = [d.user_remark, self.remark]
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
"custom_remarks",
|
"custom_remarks",
|
||||||
"remarks",
|
"remarks",
|
||||||
"base_in_words",
|
"base_in_words",
|
||||||
|
"is_opening",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"print_heading",
|
"print_heading",
|
||||||
@@ -778,6 +779,16 @@
|
|||||||
"label": "Reconcile on Advance Payment Date",
|
"label": "Reconcile on Advance Payment Date",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "No",
|
||||||
|
"depends_on": "eval: doc.book_advance_payments_in_separate_party_account == 1",
|
||||||
|
"fieldname": "is_opening",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Is Opening",
|
||||||
|
"options": "No\nYes",
|
||||||
|
"print_hide": 1,
|
||||||
|
"search_index": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -791,7 +802,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-05-17 10:21:11.199445",
|
"modified": "2024-05-31 17:07:06.197249",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
|||||||
@@ -199,11 +199,13 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
self.book_advance_payments_in_separate_party_account = False
|
self.book_advance_payments_in_separate_party_account = False
|
||||||
if self.party_type not in ("Customer", "Supplier"):
|
if self.party_type not in ("Customer", "Supplier"):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
if not frappe.db.get_value(
|
if not frappe.db.get_value(
|
||||||
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
||||||
):
|
):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
# Important to set this flag for the gl building logic to work properly
|
# Important to set this flag for the gl building logic to work properly
|
||||||
@@ -215,6 +217,7 @@ class PaymentEntry(AccountsController):
|
|||||||
if (account_type == "Payable" and self.party_type == "Customer") or (
|
if (account_type == "Payable" and self.party_type == "Customer") or (
|
||||||
account_type == "Receivable" and self.party_type == "Supplier"
|
account_type == "Receivable" and self.party_type == "Supplier"
|
||||||
):
|
):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.references:
|
if self.references:
|
||||||
@@ -224,6 +227,7 @@ class PaymentEntry(AccountsController):
|
|||||||
# If there are referencers other than `allowed_types`, treat this as a normal payment entry
|
# If there are referencers other than `allowed_types`, treat this as a normal payment entry
|
||||||
if reference_types - allowed_types:
|
if reference_types - allowed_types:
|
||||||
self.book_advance_payments_in_separate_party_account = False
|
self.book_advance_payments_in_separate_party_account = False
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
liability_account = get_party_account(
|
liability_account = get_party_account(
|
||||||
|
|||||||
@@ -1729,6 +1729,68 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.check_gl_entries()
|
self.check_gl_entries()
|
||||||
self.check_pl_entries()
|
self.check_pl_entries()
|
||||||
|
|
||||||
|
def test_opening_flag_for_advance_as_liability(self):
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
advance_account = create_account(
|
||||||
|
parent_account="Current Assets - _TC",
|
||||||
|
account_name="Advances Received",
|
||||||
|
company=company,
|
||||||
|
account_type="Receivable",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable Advance in separate party account
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 1,
|
||||||
|
"default_advance_received_account": advance_account,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Advance Payment
|
||||||
|
adv = create_payment_entry(
|
||||||
|
party_type="Customer",
|
||||||
|
party="_Test Customer",
|
||||||
|
payment_type="Receive",
|
||||||
|
paid_from="Debtors - _TC",
|
||||||
|
paid_to="_Test Cash - _TC",
|
||||||
|
)
|
||||||
|
adv.is_opening = "Yes"
|
||||||
|
adv.save() # use save() to trigger set_liability_account()
|
||||||
|
adv.submit()
|
||||||
|
|
||||||
|
gl_with_opening_set = frappe.db.get_all(
|
||||||
|
"GL Entry", filters={"voucher_no": adv.name, "is_opening": "Yes"}
|
||||||
|
)
|
||||||
|
# 'Is Opening' can be 'Yes' for Advances in separate party account
|
||||||
|
self.assertNotEqual(gl_with_opening_set, [])
|
||||||
|
|
||||||
|
# Disable Advance in separate party account
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 0,
|
||||||
|
"default_advance_received_account": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
payment = create_payment_entry(
|
||||||
|
party_type="Customer",
|
||||||
|
party="_Test Customer",
|
||||||
|
payment_type="Receive",
|
||||||
|
paid_from="Debtors - _TC",
|
||||||
|
paid_to="_Test Cash - _TC",
|
||||||
|
)
|
||||||
|
payment.is_opening = "Yes"
|
||||||
|
payment.save()
|
||||||
|
payment.submit()
|
||||||
|
gl_with_opening_set = frappe.db.get_all(
|
||||||
|
"GL Entry", filters={"voucher_no": payment.name, "is_opening": "Yes"}
|
||||||
|
)
|
||||||
|
# 'Is Opening' should always be 'No' for normal advance payments
|
||||||
|
self.assertEqual(gl_with_opening_set, [])
|
||||||
|
|
||||||
|
|
||||||
def create_payment_entry(**args):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
@@ -13,7 +14,12 @@ from erpnext.setup.utils import get_exchange_rate
|
|||||||
|
|
||||||
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
||||||
|
|
||||||
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
PAYMENT_URL = "https://example.com/payment"
|
||||||
|
|
||||||
|
payment_gateways = [
|
||||||
|
{"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
|
||||||
|
{"doctype": "Payment Gateway", "gateway": "_Test Gateway Phone"},
|
||||||
|
]
|
||||||
|
|
||||||
payment_method = [
|
payment_method = [
|
||||||
{
|
{
|
||||||
@@ -29,13 +35,21 @@ payment_method = [
|
|||||||
"payment_account": "_Test Bank USD - _TC",
|
"payment_account": "_Test Bank USD - _TC",
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"doctype": "Payment Gateway Account",
|
||||||
|
"payment_gateway": "_Test Gateway Phone",
|
||||||
|
"payment_account": "_Test Bank USD - _TC",
|
||||||
|
"payment_channel": "Phone",
|
||||||
|
"currency": "USD",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestPaymentRequest(unittest.TestCase):
|
class TestPaymentRequest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
for payment_gateway in payment_gateways:
|
||||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||||
|
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||||
|
|
||||||
for method in payment_method:
|
for method in payment_method:
|
||||||
if not frappe.db.get_value(
|
if not frappe.db.get_value(
|
||||||
@@ -45,6 +59,25 @@ class TestPaymentRequest(unittest.TestCase):
|
|||||||
):
|
):
|
||||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
send_email = patch(
|
||||||
|
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
|
||||||
|
return_value=None,
|
||||||
|
)
|
||||||
|
self.send_email = send_email.start()
|
||||||
|
self.addCleanup(send_email.stop)
|
||||||
|
get_payment_url = patch(
|
||||||
|
# this also shadows one (1) call to _get_payment_gateway_controller
|
||||||
|
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
|
||||||
|
return_value=PAYMENT_URL,
|
||||||
|
)
|
||||||
|
self.get_payment_url = get_payment_url.start()
|
||||||
|
self.addCleanup(get_payment_url.stop)
|
||||||
|
_get_payment_gateway_controller = patch(
|
||||||
|
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
|
||||||
|
)
|
||||||
|
self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
|
||||||
|
self.addCleanup(_get_payment_gateway_controller.stop)
|
||||||
|
|
||||||
def test_payment_request_linkings(self):
|
def test_payment_request_linkings(self):
|
||||||
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
||||||
so_inr.disable_rounded_total = 1
|
so_inr.disable_rounded_total = 1
|
||||||
@@ -75,6 +108,83 @@ class TestPaymentRequest(unittest.TestCase):
|
|||||||
self.assertEqual(pr.reference_name, si_usd.name)
|
self.assertEqual(pr.reference_name, si_usd.name)
|
||||||
self.assertEqual(pr.currency, "USD")
|
self.assertEqual(pr.currency, "USD")
|
||||||
|
|
||||||
|
def test_payment_channels(self):
|
||||||
|
so = make_sales_order(currency="USD")
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||||
|
submit_doc=False,
|
||||||
|
return_doc=True,
|
||||||
|
)
|
||||||
|
pr.flags.mute_email = True # but temporarily prohibit sending
|
||||||
|
pr.submit()
|
||||||
|
pr.reload()
|
||||||
|
self.assertEqual(pr.payment_channel, "Email")
|
||||||
|
self.assertEqual(pr.mute_email, False)
|
||||||
|
|
||||||
|
self.assertIsNone(pr.payment_url)
|
||||||
|
self.assertEqual(self.send_email.call_count, 0) # hence: no increment
|
||||||
|
self.assertEqual(self._get_payment_gateway_controller.call_count, 1)
|
||||||
|
pr.cancel()
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
payment_gateway_account="_Test Gateway Phone - USD",
|
||||||
|
submit_doc=True,
|
||||||
|
return_doc=True,
|
||||||
|
)
|
||||||
|
pr.reload()
|
||||||
|
|
||||||
|
self.assertEqual(pr.payment_channel, "Phone")
|
||||||
|
self.assertEqual(pr.mute_email, False)
|
||||||
|
|
||||||
|
self.assertIsNone(pr.payment_url)
|
||||||
|
self.assertEqual(self.send_email.call_count, 0) # no increment on phone channel
|
||||||
|
self.assertEqual(self._get_payment_gateway_controller.call_count, 3)
|
||||||
|
pr.cancel()
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||||
|
submit_doc=True,
|
||||||
|
return_doc=True,
|
||||||
|
)
|
||||||
|
pr.reload()
|
||||||
|
|
||||||
|
self.assertEqual(pr.payment_channel, "Email")
|
||||||
|
self.assertEqual(pr.mute_email, False)
|
||||||
|
|
||||||
|
self.assertEqual(pr.payment_url, PAYMENT_URL)
|
||||||
|
self.assertEqual(self.send_email.call_count, 1) # increment on normal email channel
|
||||||
|
self.assertEqual(self._get_payment_gateway_controller.call_count, 4)
|
||||||
|
pr.cancel()
|
||||||
|
|
||||||
|
so = make_sales_order(currency="USD", do_not_save=True)
|
||||||
|
# no-op; for optical consistency with how a webshop SO would look like
|
||||||
|
so.order_type = "Shopping Cart"
|
||||||
|
so.save()
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||||
|
order_type="Shopping Cart",
|
||||||
|
submit_doc=True,
|
||||||
|
return_doc=True,
|
||||||
|
)
|
||||||
|
pr.reload()
|
||||||
|
|
||||||
|
self.assertEqual(pr.payment_channel, "Email")
|
||||||
|
self.assertEqual(pr.mute_email, False)
|
||||||
|
|
||||||
|
self.assertIsNone(pr.payment_url)
|
||||||
|
self.assertEqual(self.send_email.call_count, 1) # no increment on shopping cart
|
||||||
|
self.assertEqual(self._get_payment_gateway_controller.call_count, 5)
|
||||||
|
pr.cancel()
|
||||||
|
|
||||||
def test_payment_entry_against_purchase_invoice(self):
|
def test_payment_entry_against_purchase_invoice(self):
|
||||||
si_usd = make_purchase_invoice(
|
si_usd = make_purchase_invoice(
|
||||||
customer="_Test Supplier USD",
|
customer="_Test Supplier USD",
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
for key, value in pos_occurences.items():
|
for key, value in pos_occurences.items():
|
||||||
if len(value) > 1:
|
if len(value) > 1:
|
||||||
error_list.append(
|
error_list.append(
|
||||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||||
)
|
)
|
||||||
|
|
||||||
if error_list:
|
if error_list:
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.apply_loyalty_points()
|
self.apply_loyalty_points()
|
||||||
self.check_phone_payments()
|
self.check_phone_payments()
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
||||||
|
self.make_bundle_for_sales_purchase_return()
|
||||||
self.submit_serial_batch_bundle()
|
self.submit_serial_batch_bundle()
|
||||||
|
|
||||||
if self.coupon_code:
|
if self.coupon_code:
|
||||||
|
|||||||
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
|
pos.reload()
|
||||||
|
|
||||||
pos_return1 = make_sales_return(pos.name)
|
pos_return1 = make_sales_return(pos.name)
|
||||||
|
|
||||||
# partial return 1
|
# partial return 1
|
||||||
pos_return1.get("items")[0].qty = -1
|
pos_return1.get("items")[0].qty = -1
|
||||||
|
pos_return1.submit()
|
||||||
|
pos_return1.reload()
|
||||||
|
|
||||||
bundle_id = frappe.get_doc(
|
bundle_id = frappe.get_doc(
|
||||||
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
||||||
)
|
)
|
||||||
|
|
||||||
bundle_id.remove(bundle_id.entries[1])
|
|
||||||
bundle_id.save()
|
|
||||||
|
|
||||||
bundle_id.load_from_db()
|
bundle_id.load_from_db()
|
||||||
|
|
||||||
serial_no = bundle_id.entries[0].serial_no
|
serial_no = bundle_id.entries[0].serial_no
|
||||||
self.assertEqual(serial_no, serial_nos[0])
|
self.assertEqual(serial_no, serial_nos[0])
|
||||||
|
|
||||||
pos_return1.insert()
|
|
||||||
pos_return1.submit()
|
|
||||||
|
|
||||||
# partial return 2
|
# partial return 2
|
||||||
pos_return2 = make_sales_return(pos.name)
|
pos_return2 = make_sales_return(pos.name)
|
||||||
|
pos_return2.submit()
|
||||||
|
|
||||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||||
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
||||||
self.assertEqual(serial_no, serial_nos[1])
|
self.assertEqual(serial_no, serial_nos[1])
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
|
|||||||
for key, value in pos_occurences.items():
|
for key, value in pos_occurences.items():
|
||||||
if len(value) > 1:
|
if len(value) > 1:
|
||||||
error_list.append(
|
error_list.append(
|
||||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||||
)
|
)
|
||||||
|
|
||||||
if error_list:
|
if error_list:
|
||||||
|
|||||||
@@ -929,6 +929,30 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
for doc in [si, si1]:
|
for doc in [si, si1]:
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
|
def test_pricing_rule_for_transaction_with_condition(self):
|
||||||
|
make_item("PR Transaction Condition")
|
||||||
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||||
|
make_pricing_rule(
|
||||||
|
selling=1,
|
||||||
|
min_qty=0,
|
||||||
|
price_or_product_discount="Product",
|
||||||
|
apply_on="Transaction",
|
||||||
|
free_item="PR Transaction Condition",
|
||||||
|
free_qty=1,
|
||||||
|
free_item_rate=10,
|
||||||
|
condition="customer=='_Test Customer 1'",
|
||||||
|
)
|
||||||
|
|
||||||
|
si = create_sales_invoice(qty=5, customer="_Test Customer 1", do_not_submit=True)
|
||||||
|
self.assertEqual(len(si.items), 2)
|
||||||
|
self.assertEqual(si.items[1].rate, 10)
|
||||||
|
|
||||||
|
si1 = create_sales_invoice(qty=5, customer="_Test Customer 2", do_not_submit=True)
|
||||||
|
self.assertEqual(len(si1.items), 1)
|
||||||
|
|
||||||
|
for doc in [si, si1]:
|
||||||
|
doc.delete()
|
||||||
|
|
||||||
def test_remove_pricing_rule(self):
|
def test_remove_pricing_rule(self):
|
||||||
item = make_item("Water Flask")
|
item = make_item("Water Flask")
|
||||||
make_item_price("Water Flask", "_Test Price List", 100)
|
make_item_price("Water Flask", "_Test Price List", 100)
|
||||||
|
|||||||
@@ -564,6 +564,7 @@ def apply_pricing_rule_on_transaction(doc):
|
|||||||
|
|
||||||
if pricing_rules:
|
if pricing_rules:
|
||||||
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
|
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
|
||||||
|
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
|
||||||
|
|
||||||
if not pricing_rules:
|
if not pricing_rules:
|
||||||
remove_free_item(doc)
|
remove_free_item(doc)
|
||||||
|
|||||||
@@ -681,7 +681,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
if (frm.doc.supplier) {
|
if (frm.doc.supplier) {
|
||||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||||
}
|
}
|
||||||
if (!frm.doc.__onload.supplier_tds) {
|
if (!frm.doc.__onload.enable_apply_tds) {
|
||||||
frm.set_df_property("apply_tds", "read_only", 1);
|
frm.set_df_property("apply_tds", "read_only", 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,6 +348,22 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.tax_withholding_category = tds_category
|
self.tax_withholding_category = tds_category
|
||||||
self.set_onload("supplier_tds", tds_category)
|
self.set_onload("supplier_tds", tds_category)
|
||||||
|
|
||||||
|
# If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
|
||||||
|
if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
|
||||||
|
po = qb.DocType("Purchase Order")
|
||||||
|
po_with_tds = (
|
||||||
|
qb.from_(po)
|
||||||
|
.select(po.name)
|
||||||
|
.where(
|
||||||
|
po.docstatus.eq(1)
|
||||||
|
& (po.name.isin(purchase_orders))
|
||||||
|
& (po.apply_tds.eq(1))
|
||||||
|
& (po.tax_withholding_category.notnull())
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
self.set_onload("enable_apply_tds", True if po_with_tds else False)
|
||||||
|
|
||||||
super().set_missing_values(for_validate)
|
super().set_missing_values(for_validate)
|
||||||
|
|
||||||
def validate_credit_to_acc(self):
|
def validate_credit_to_acc(self):
|
||||||
@@ -449,7 +465,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||||
stock_items = self.get_stock_items()
|
stock_items = self.get_stock_items()
|
||||||
|
|
||||||
asset_received_but_not_billed = None
|
self.asset_received_but_not_billed = None
|
||||||
|
|
||||||
if self.update_stock:
|
if self.update_stock:
|
||||||
self.validate_item_code()
|
self.validate_item_code()
|
||||||
@@ -532,26 +548,45 @@ class PurchaseInvoice(BuyingController):
|
|||||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||||
|
|
||||||
item.expense_account = stock_not_billed_account
|
item.expense_account = stock_not_billed_account
|
||||||
elif item.is_fixed_asset and item.pr_detail:
|
|
||||||
if not asset_received_but_not_billed:
|
|
||||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
|
||||||
item.expense_account = asset_received_but_not_billed
|
|
||||||
elif item.is_fixed_asset:
|
elif item.is_fixed_asset:
|
||||||
account_type = (
|
account = None
|
||||||
"capital_work_in_progress_account"
|
if item.pr_detail:
|
||||||
if is_cwip_accounting_enabled(item.asset_category)
|
if not self.asset_received_but_not_billed:
|
||||||
else "fixed_asset_account"
|
self.asset_received_but_not_billed = self.get_company_default(
|
||||||
)
|
"asset_received_but_not_billed"
|
||||||
asset_category_account = get_asset_category_account(
|
)
|
||||||
account_type, item=item.item_code, company=self.company
|
|
||||||
)
|
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
|
||||||
if not asset_category_account:
|
arbnb_booked_in_pr = frappe.db.get_value(
|
||||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
"GL Entry",
|
||||||
throw(
|
{
|
||||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
"voucher_type": "Purchase Receipt",
|
||||||
title=_("Missing Account"),
|
"voucher_no": item.purchase_receipt,
|
||||||
|
"account": self.asset_received_but_not_billed,
|
||||||
|
},
|
||||||
|
"name",
|
||||||
)
|
)
|
||||||
item.expense_account = asset_category_account
|
if arbnb_booked_in_pr:
|
||||||
|
account = self.asset_received_but_not_billed
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
account_type = (
|
||||||
|
"capital_work_in_progress_account"
|
||||||
|
if is_cwip_accounting_enabled(item.asset_category)
|
||||||
|
else "fixed_asset_account"
|
||||||
|
)
|
||||||
|
account = get_asset_category_account(
|
||||||
|
account_type, item=item.item_code, company=self.company
|
||||||
|
)
|
||||||
|
if not account:
|
||||||
|
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||||
|
throw(
|
||||||
|
_("Please set Fixed Asset Account in {} against {}.").format(
|
||||||
|
form_link, self.company
|
||||||
|
),
|
||||||
|
title=_("Missing Account"),
|
||||||
|
)
|
||||||
|
item.expense_account = account
|
||||||
elif not item.expense_account and for_validate:
|
elif not item.expense_account and for_validate:
|
||||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||||
|
|
||||||
@@ -708,6 +743,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||||
if self.update_stock == 1:
|
if self.update_stock == 1:
|
||||||
|
self.make_bundle_for_sales_purchase_return()
|
||||||
self.make_bundle_using_old_serial_batch_fields()
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
|
|||||||
@@ -53,13 +53,15 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-05-23 17:00:42.984798",
|
"modified": "2024-06-03 17:30:37.012593",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Accounting Ledger",
|
"name": "Repost Accounting Ledger",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
@@ -68,7 +70,9 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:32.287007",
|
"modified": "2024-06-06 13:56:37.908879",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Accounting Ledger Settings",
|
"name": "Repost Accounting Ledger Settings",
|
||||||
@@ -30,13 +30,17 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Administrator",
|
"role": "Administrator",
|
||||||
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"select": 1
|
"select": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
|
|||||||
@@ -98,13 +98,15 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-05-23 17:00:31.540640",
|
"modified": "2024-06-03 17:31:04.472279",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Payment Ledger",
|
"name": "Repost Payment Ledger",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
@@ -113,7 +115,9 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ class SalesInvoice(SellingController):
|
|||||||
if not self.get(table_name):
|
if not self.get(table_name):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
self.make_bundle_for_sales_purchase_return(table_name)
|
||||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
@@ -2643,6 +2644,10 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
target.closing_text = letter_text.get("closing_text")
|
target.closing_text = letter_text.get("closing_text")
|
||||||
target.language = letter_text.get("language")
|
target.language = letter_text.get("language")
|
||||||
|
|
||||||
|
# update outstanding
|
||||||
|
if source.payment_schedule and len(source.payment_schedule) == 1:
|
||||||
|
target.overdue_payments[0].outstanding = source.get("outstanding_amount")
|
||||||
|
|
||||||
target.validate()
|
target.validate()
|
||||||
|
|
||||||
return get_mapped_doc(
|
return get_mapped_doc(
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ def get_conditions(filters):
|
|||||||
|
|
||||||
if filters.account_type:
|
if filters.account_type:
|
||||||
conditions["account_type"] = filters.account_type
|
conditions["account_type"] = filters.account_type
|
||||||
return conditions
|
|
||||||
|
|
||||||
if filters.company:
|
if filters.company:
|
||||||
conditions["company"] = filters.company
|
conditions["company"] = filters.company
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder import DocType
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
|
||||||
|
|
||||||
@@ -75,11 +76,24 @@ def get_data(filters):
|
|||||||
asset_data = assets_details.get(d.against_voucher)
|
asset_data = assets_details.get(d.against_voucher)
|
||||||
if asset_data:
|
if asset_data:
|
||||||
if not asset_data.get("accumulated_depreciation_amount"):
|
if not asset_data.get("accumulated_depreciation_amount"):
|
||||||
asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
|
AssetDepreciationSchedule = DocType("Asset Depreciation Schedule")
|
||||||
"opening_accumulated_depreciation"
|
DepreciationSchedule = DocType("Depreciation Schedule")
|
||||||
)
|
query = (
|
||||||
|
frappe.qb.from_(DepreciationSchedule)
|
||||||
|
.join(AssetDepreciationSchedule)
|
||||||
|
.on(DepreciationSchedule.parent == AssetDepreciationSchedule.name)
|
||||||
|
.select(DepreciationSchedule.accumulated_depreciation_amount)
|
||||||
|
.where(
|
||||||
|
(AssetDepreciationSchedule.asset == d.against_voucher)
|
||||||
|
& (DepreciationSchedule.parenttype == "Asset Depreciation Schedule")
|
||||||
|
& (DepreciationSchedule.schedule_date == d.posting_date)
|
||||||
|
)
|
||||||
|
).run(as_dict=True)
|
||||||
|
asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
asset_data.accumulated_depreciation_amount += d.debit
|
asset_data.accumulated_depreciation_amount += d.debit
|
||||||
|
asset_data.opening_accumulated_depreciation = asset_data.accumulated_depreciation_amount - d.debit
|
||||||
|
|
||||||
row = frappe._dict(asset_data)
|
row = frappe._dict(asset_data)
|
||||||
row.update(
|
row.update(
|
||||||
|
|||||||
@@ -69,48 +69,50 @@ def get_asset_categories_for_grouped_by_category(filters):
|
|||||||
condition = ""
|
condition = ""
|
||||||
if filters.get("asset_category"):
|
if filters.get("asset_category"):
|
||||||
condition += " and asset_category = %(asset_category)s"
|
condition += " and asset_category = %(asset_category)s"
|
||||||
|
# nosemgrep
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
SELECT asset_category,
|
SELECT a.asset_category,
|
||||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_as_on_from_date,
|
end), 0) as cost_as_on_from_date,
|
||||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_new_purchase,
|
end), 0) as cost_of_new_purchase,
|
||||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||||
and disposal_date >= %(from_date)s
|
and a.disposal_date >= %(from_date)s
|
||||||
and disposal_date <= %(to_date)s then
|
and a.disposal_date <= %(to_date)s then
|
||||||
case when status = "Sold" then
|
case when a.status = "Sold" then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_sold_asset,
|
end), 0) as cost_of_sold_asset,
|
||||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||||
and disposal_date >= %(from_date)s
|
and a.disposal_date >= %(from_date)s
|
||||||
and disposal_date <= %(to_date)s then
|
and a.disposal_date <= %(to_date)s then
|
||||||
case when status = "Scrapped" then
|
case when a.status = "Scrapped" then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_scrapped_asset
|
end), 0) as cost_of_scrapped_asset
|
||||||
from `tabAsset`
|
from `tabAsset` a
|
||||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
||||||
group by asset_category
|
and not exists(select name from `tabAsset Capitalization Asset Item` where asset = a.name)
|
||||||
|
group by a.asset_category
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"to_date": filters.to_date,
|
"to_date": filters.to_date,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def get_provisional_profit_loss(
|
|||||||
):
|
):
|
||||||
provisional_profit_loss = {}
|
provisional_profit_loss = {}
|
||||||
total_row = {}
|
total_row = {}
|
||||||
if asset and (liability or equity):
|
if asset:
|
||||||
total = total_row_total = 0
|
total = total_row_total = 0
|
||||||
currency = currency or frappe.get_cached_value("Company", company, "default_currency")
|
currency = currency or frappe.get_cached_value("Company", company, "default_currency")
|
||||||
total_row = {
|
total_row = {
|
||||||
@@ -122,14 +122,20 @@ def get_provisional_profit_loss(
|
|||||||
|
|
||||||
for period in period_list:
|
for period in period_list:
|
||||||
key = period if consolidated else period.key
|
key = period if consolidated else period.key
|
||||||
effective_liability = 0.0
|
total_assets = flt(asset[0].get(key))
|
||||||
if liability:
|
|
||||||
effective_liability += flt(liability[0].get(key))
|
|
||||||
if equity:
|
|
||||||
effective_liability += flt(equity[0].get(key))
|
|
||||||
|
|
||||||
provisional_profit_loss[key] = flt(asset[0].get(key)) - effective_liability
|
if liability or equity:
|
||||||
total_row[key] = effective_liability + provisional_profit_loss[key]
|
effective_liability = 0.0
|
||||||
|
if liability:
|
||||||
|
effective_liability += flt(liability[0].get(key))
|
||||||
|
if equity:
|
||||||
|
effective_liability += flt(equity[0].get(key))
|
||||||
|
|
||||||
|
provisional_profit_loss[key] = total_assets - effective_liability
|
||||||
|
else:
|
||||||
|
provisional_profit_loss[key] = total_assets
|
||||||
|
|
||||||
|
total_row[key] = provisional_profit_loss[key]
|
||||||
|
|
||||||
if provisional_profit_loss[key]:
|
if provisional_profit_loss[key]:
|
||||||
has_value = True
|
has_value = True
|
||||||
|
|||||||
@@ -421,6 +421,8 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
if filters.get("show_net_values_in_party_account"):
|
if filters.get("show_net_values_in_party_account"):
|
||||||
account_type_map = get_account_type_map(filters.get("company"))
|
account_type_map = get_account_type_map(filters.get("company"))
|
||||||
|
|
||||||
|
immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
|
||||||
|
|
||||||
def update_value_in_dict(data, key, gle):
|
def update_value_in_dict(data, key, gle):
|
||||||
data[key].debit += gle.debit
|
data[key].debit += gle.debit
|
||||||
data[key].credit += gle.credit
|
data[key].credit += gle.credit
|
||||||
@@ -485,12 +487,17 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
|
|
||||||
elif group_by_voucher_consolidated:
|
elif group_by_voucher_consolidated:
|
||||||
keylist = [
|
keylist = [
|
||||||
|
gle.get("posting_date"),
|
||||||
gle.get("voucher_type"),
|
gle.get("voucher_type"),
|
||||||
gle.get("voucher_no"),
|
gle.get("voucher_no"),
|
||||||
gle.get("account"),
|
gle.get("account"),
|
||||||
gle.get("party_type"),
|
gle.get("party_type"),
|
||||||
gle.get("party"),
|
gle.get("party"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if immutable_ledger:
|
||||||
|
keylist.append(gle.get("creation"))
|
||||||
|
|
||||||
if filters.get("include_dimensions"):
|
if filters.get("include_dimensions"):
|
||||||
for dim in accounting_dimensions:
|
for dim in accounting_dimensions:
|
||||||
keylist.append(gle.get(dim))
|
keylist.append(gle.get(dim))
|
||||||
|
|||||||
@@ -5,14 +5,15 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
from pypika import Order
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||||
add_sub_total_row,
|
add_sub_total_row,
|
||||||
add_total_row,
|
add_total_row,
|
||||||
|
apply_group_by_conditions,
|
||||||
get_grand_total,
|
get_grand_total,
|
||||||
get_group_by_and_display_fields,
|
get_group_by_and_display_fields,
|
||||||
get_group_by_conditions,
|
|
||||||
get_tax_accounts,
|
get_tax_accounts,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||||
@@ -29,7 +30,7 @@ def _execute(filters=None, additional_table_columns=None):
|
|||||||
|
|
||||||
company_currency = erpnext.get_company_currency(filters.company)
|
company_currency = erpnext.get_company_currency(filters.company)
|
||||||
|
|
||||||
item_list = get_items(filters, get_query_columns(additional_table_columns))
|
item_list = get_items(filters, additional_table_columns)
|
||||||
aii_account_map = get_aii_accounts()
|
aii_account_map = get_aii_accounts()
|
||||||
if item_list:
|
if item_list:
|
||||||
itemised_tax, tax_columns = get_tax_accounts(
|
itemised_tax, tax_columns = get_tax_accounts(
|
||||||
@@ -287,59 +288,87 @@ def get_columns(additional_table_columns, filters):
|
|||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filters):
|
def apply_conditions(query, pi, pii, filters):
|
||||||
conditions = ""
|
for opts in ("company", "supplier", "item_code", "mode_of_payment"):
|
||||||
|
if filters.get(opts):
|
||||||
|
query = query.where(pi[opts] == filters[opts])
|
||||||
|
|
||||||
for opts in (
|
if filters.get("from_date"):
|
||||||
("company", " and `tabPurchase Invoice`.company=%(company)s"),
|
query = query.where(pi.posting_date >= filters.get("from_date"))
|
||||||
("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"),
|
|
||||||
("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"),
|
if filters.get("to_date"):
|
||||||
("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"),
|
query = query.where(pi.posting_date <= filters.get("to_date"))
|
||||||
("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"),
|
|
||||||
("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"),
|
if filters.get("item_group"):
|
||||||
("item_group", " and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s"),
|
query = query.where(pii.item_group == filters.get("item_group"))
|
||||||
):
|
|
||||||
if filters.get(opts[0]):
|
|
||||||
conditions += opts[1]
|
|
||||||
|
|
||||||
if not filters.get("group_by"):
|
if not filters.get("group_by"):
|
||||||
conditions += (
|
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||||
"ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc"
|
query = query.orderby(pii.item_group, order=Order.desc)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
conditions += get_group_by_conditions(filters, "Purchase Invoice")
|
query = apply_group_by_conditions(filters, "Purchase Invoice")
|
||||||
|
|
||||||
return conditions
|
return query
|
||||||
|
|
||||||
|
|
||||||
def get_items(filters, additional_query_columns):
|
def get_items(filters, additional_table_columns):
|
||||||
conditions = get_conditions(filters)
|
pi = frappe.qb.DocType("Purchase Invoice")
|
||||||
if additional_query_columns:
|
pii = frappe.qb.DocType("Purchase Invoice Item")
|
||||||
additional_query_columns = "," + ",".join(additional_query_columns)
|
Item = frappe.qb.DocType("Item")
|
||||||
return frappe.db.sql(
|
query = (
|
||||||
f"""
|
frappe.qb.from_(pi)
|
||||||
select
|
.join(pii)
|
||||||
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
|
.on(pi.name == pii.parent)
|
||||||
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
|
.left_join(Item)
|
||||||
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
|
.on(pii.item_code == Item.name)
|
||||||
`tabPurchase Invoice`.unrealized_profit_loss_account,
|
.select(
|
||||||
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_group`,
|
pii.name.as_("pii_name"),
|
||||||
`tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group,
|
pii.parent,
|
||||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
pi.posting_date,
|
||||||
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
|
pi.credit_to,
|
||||||
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
|
pi.company,
|
||||||
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
|
pi.supplier,
|
||||||
`tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
|
pi.remarks,
|
||||||
`tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {additional_query_columns}
|
pi.base_net_total,
|
||||||
from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
|
pi.unrealized_profit_loss_account,
|
||||||
where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
|
pii.item_code,
|
||||||
`tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
|
pii.description,
|
||||||
`tabPurchase Invoice`.docstatus = 1 {conditions}
|
pii.item_group,
|
||||||
""",
|
pii.item_name.as_("pi_item_name"),
|
||||||
filters,
|
pii.item_group.as_("pi_item_group"),
|
||||||
as_dict=1,
|
Item.item_name.as_("i_item_name"),
|
||||||
|
Item.item_group.as_("i_item_group"),
|
||||||
|
pii.project,
|
||||||
|
pii.purchase_order,
|
||||||
|
pii.purchase_receipt,
|
||||||
|
pii.po_detail,
|
||||||
|
pii.expense_account,
|
||||||
|
pii.stock_qty,
|
||||||
|
pii.stock_uom,
|
||||||
|
pii.base_net_amount,
|
||||||
|
pi.supplier_name,
|
||||||
|
pi.mode_of_payment,
|
||||||
|
)
|
||||||
|
.where(pi.docstatus == 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if filters.get("supplier"):
|
||||||
|
query = query.where(pi.supplier == filters["supplier"])
|
||||||
|
if filters.get("company"):
|
||||||
|
query = query.where(pi.company == filters["company"])
|
||||||
|
|
||||||
|
if additional_table_columns:
|
||||||
|
for column in additional_table_columns:
|
||||||
|
if column.get("_doctype"):
|
||||||
|
table = frappe.qb.DocType(column.get("_doctype"))
|
||||||
|
query = query.select(table[column.get("fieldname")])
|
||||||
|
else:
|
||||||
|
query = query.select(pi[column.get("fieldname")])
|
||||||
|
|
||||||
|
query = apply_conditions(query, pi, pii, filters)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_aii_accounts():
|
def get_aii_accounts():
|
||||||
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
|
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
|||||||
label: __("Warehouse"),
|
label: __("Warehouse"),
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
options: "Warehouse",
|
options: "Warehouse",
|
||||||
|
get_query: function () {
|
||||||
|
const company = frappe.query_report.get_filter_value("company");
|
||||||
|
return {
|
||||||
|
filters: { company: company },
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "brand",
|
fieldname: "brand",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from frappe import _
|
|||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
from frappe.utils.xlsxutils import handle_html
|
from frappe.utils.xlsxutils import handle_html
|
||||||
|
from pypika import Order
|
||||||
|
|
||||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||||
@@ -26,7 +27,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
|||||||
|
|
||||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||||
|
|
||||||
item_list = get_items(filters, get_query_columns(additional_table_columns), additional_conditions)
|
item_list = get_items(filters, additional_table_columns, additional_conditions)
|
||||||
if item_list:
|
if item_list:
|
||||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||||
|
|
||||||
@@ -340,101 +341,140 @@ def get_columns(additional_table_columns, filters):
|
|||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filters, additional_conditions=None):
|
def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||||
conditions = ""
|
for opts in ("company", "customer", "item_code"):
|
||||||
|
if filters.get(opts):
|
||||||
|
query = query.where(si[opts] == filters[opts])
|
||||||
|
|
||||||
for opts in (
|
if filters.get("from_date"):
|
||||||
("company", " and `tabSales Invoice`.company=%(company)s"),
|
query = query.where(si.posting_date >= filters.get("from_date"))
|
||||||
("customer", " and `tabSales Invoice`.customer = %(customer)s"),
|
|
||||||
("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"),
|
|
||||||
("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"),
|
|
||||||
("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s"),
|
|
||||||
):
|
|
||||||
if filters.get(opts[0]):
|
|
||||||
conditions += opts[1]
|
|
||||||
|
|
||||||
if additional_conditions:
|
if filters.get("to_date"):
|
||||||
conditions += additional_conditions
|
query = query.where(si.posting_date <= filters.get("to_date"))
|
||||||
|
|
||||||
if filters.get("mode_of_payment"):
|
if filters.get("mode_of_payment"):
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
sales_invoice = frappe.db.get_all(
|
||||||
where parent=`tabSales Invoice`.name
|
"Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
|
||||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
)
|
||||||
|
query = query.where(si.name.isin(sales_invoice))
|
||||||
|
|
||||||
if filters.get("warehouse"):
|
if filters.get("warehouse"):
|
||||||
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
||||||
lft, rgt = frappe.db.get_all(
|
lft, rgt = frappe.db.get_all(
|
||||||
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
|
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
|
||||||
)[0]
|
)[0]
|
||||||
conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) "
|
warehouses = frappe.db.get_all("Warehouse", {"lft": (">", lft), "rgt": ("<", rgt)}, pluck="name")
|
||||||
|
query = query.where(sii.warehouse.isin(warehouses))
|
||||||
else:
|
else:
|
||||||
conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
|
query = query.where(sii.warehouse == filters.get("warehouse"))
|
||||||
|
|
||||||
if filters.get("brand"):
|
if filters.get("brand"):
|
||||||
conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s"""
|
query = query.where(sii.brand == filters.get("brand"))
|
||||||
|
|
||||||
if filters.get("item_group"):
|
if filters.get("item_group"):
|
||||||
conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s"""
|
query = query.where(sii.item_group == filters.get("item_group"))
|
||||||
|
|
||||||
if filters.get("income_account"):
|
if filters.get("income_account"):
|
||||||
conditions += """
|
query = query.where(
|
||||||
and (ifnull(`tabSales Invoice Item`.income_account, '') = %(income_account)s
|
(sii.income_account == filters.get("income_account"))
|
||||||
or ifnull(`tabSales Invoice Item`.deferred_revenue_account, '') = %(income_account)s
|
| (sii.deferred_revenue_account == filters.get("income_account"))
|
||||||
or ifnull(`tabSales Invoice`.unrealized_profit_loss_account, '') = %(income_account)s)
|
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
||||||
"""
|
)
|
||||||
|
|
||||||
if not filters.get("group_by"):
|
if not filters.get("group_by"):
|
||||||
conditions += "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc"
|
query = query.orderby(si.posting_date, order=Order.desc)
|
||||||
|
query = query.orderby(sii.item_group, order=Order.desc)
|
||||||
else:
|
else:
|
||||||
conditions += get_group_by_conditions(filters, "Sales Invoice")
|
query = apply_group_by_conditions(query, si, sii, filters)
|
||||||
|
|
||||||
return conditions
|
for key, value in (additional_conditions or {}).items():
|
||||||
|
query = query.where(si[key] == value)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
def get_group_by_conditions(filters, doctype):
|
def apply_group_by_conditions(query, si, ii, filters):
|
||||||
if filters.get("group_by") == "Invoice":
|
if filters.get("group_by") == "Invoice":
|
||||||
return f"ORDER BY `tab{doctype} Item`.parent desc"
|
query = query.orderby(ii.parent, order=Order.desc)
|
||||||
elif filters.get("group_by") == "Item":
|
elif filters.get("group_by") == "Item":
|
||||||
return f"ORDER BY `tab{doctype} Item`.`item_code`"
|
query = query.orderby(ii.item_code)
|
||||||
elif filters.get("group_by") == "Item Group":
|
elif filters.get("group_by") == "Item Group":
|
||||||
return "ORDER BY `tab{} Item`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
|
query = query.orderby(ii.item_group)
|
||||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||||
return "ORDER BY `tab{}`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
|
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||||
conditions = get_conditions(filters, additional_conditions)
|
si = frappe.qb.DocType("Sales Invoice")
|
||||||
|
sii = frappe.qb.DocType("Sales Invoice Item")
|
||||||
|
item = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(si)
|
||||||
|
.join(sii)
|
||||||
|
.on(si.name == sii.parent)
|
||||||
|
.left_join(item)
|
||||||
|
.on(sii.item_code == item.name)
|
||||||
|
.select(
|
||||||
|
sii.name,
|
||||||
|
sii.parent,
|
||||||
|
si.posting_date,
|
||||||
|
si.debit_to,
|
||||||
|
si.unrealized_profit_loss_account,
|
||||||
|
si.is_internal_customer,
|
||||||
|
si.customer,
|
||||||
|
si.remarks,
|
||||||
|
si.territory,
|
||||||
|
si.company,
|
||||||
|
si.base_net_total,
|
||||||
|
sii.project,
|
||||||
|
sii.item_code,
|
||||||
|
sii.description,
|
||||||
|
sii.item_name,
|
||||||
|
sii.item_group,
|
||||||
|
sii.item_name.as_("si_item_name"),
|
||||||
|
sii.item_group.as_("si_item_group"),
|
||||||
|
item.item_name.as_("i_item_name"),
|
||||||
|
item.item_group.as_("i_item_group"),
|
||||||
|
sii.sales_order,
|
||||||
|
sii.delivery_note,
|
||||||
|
sii.income_account,
|
||||||
|
sii.cost_center,
|
||||||
|
sii.enable_deferred_revenue,
|
||||||
|
sii.deferred_revenue_account,
|
||||||
|
sii.stock_qty,
|
||||||
|
sii.stock_uom,
|
||||||
|
sii.base_net_rate,
|
||||||
|
sii.base_net_amount,
|
||||||
|
si.customer_name,
|
||||||
|
si.customer_group,
|
||||||
|
sii.so_detail,
|
||||||
|
si.update_stock,
|
||||||
|
sii.uom,
|
||||||
|
sii.qty,
|
||||||
|
)
|
||||||
|
.where(si.docstatus == 1)
|
||||||
|
)
|
||||||
|
|
||||||
if additional_query_columns:
|
if additional_query_columns:
|
||||||
additional_query_columns = "," + ",".join(additional_query_columns)
|
for column in additional_query_columns:
|
||||||
return frappe.db.sql(
|
if column.get("_doctype"):
|
||||||
"""
|
table = frappe.qb.DocType(column.get("_doctype"))
|
||||||
select
|
query = query.select(table[column.get("fieldname")])
|
||||||
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
|
else:
|
||||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
query = query.select(si[column.get("fieldname")])
|
||||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
|
||||||
`tabSales Invoice`.is_internal_customer,
|
if filters.get("customer"):
|
||||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
query = query.where(si.customer == filters["customer"])
|
||||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
|
||||||
`tabSales Invoice Item`.project,
|
if filters.get("customer_group"):
|
||||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
query = query.where(si.customer_group == filters["customer_group"])
|
||||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
|
||||||
`tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group,
|
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
|
||||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
return query.run(as_dict=True)
|
||||||
`tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
|
|
||||||
`tabSales Invoice Item`.enable_deferred_revenue, `tabSales Invoice Item`.deferred_revenue_account,
|
|
||||||
`tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
|
|
||||||
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
|
|
||||||
`tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
|
|
||||||
`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {}
|
|
||||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem`
|
|
||||||
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and
|
|
||||||
`tabItem`.name = `tabSales Invoice Item`.`item_code` and
|
|
||||||
`tabSales Invoice`.docstatus = 1 {}
|
|
||||||
""".format(additional_query_columns, conditions),
|
|
||||||
filters,
|
|
||||||
as_dict=1,
|
|
||||||
) # nosec
|
|
||||||
|
|
||||||
|
|
||||||
def get_delivery_notes_against_sales_order(item_list):
|
def get_delivery_notes_against_sales_order(item_list):
|
||||||
@@ -442,16 +482,14 @@ def get_delivery_notes_against_sales_order(item_list):
|
|||||||
so_item_rows = list(set([d.so_detail for d in item_list]))
|
so_item_rows = list(set([d.so_detail for d in item_list]))
|
||||||
|
|
||||||
if so_item_rows:
|
if so_item_rows:
|
||||||
delivery_notes = frappe.db.sql(
|
dn_item = frappe.qb.DocType("Delivery Note Item")
|
||||||
"""
|
delivery_notes = (
|
||||||
select parent, so_detail
|
frappe.qb.from_(dn_item)
|
||||||
from `tabDelivery Note Item`
|
.select(dn_item.parent, dn_item.so_detail)
|
||||||
where docstatus=1 and so_detail in (%s)
|
.where(dn_item.docstatus == 1)
|
||||||
group by so_detail, parent
|
.where(dn_item.so_detail.isin(so_item_rows))
|
||||||
"""
|
.groupby(dn_item.so_detail, dn_item.parent)
|
||||||
% (", ".join(["%s"] * len(so_item_rows))),
|
.run(as_dict=True)
|
||||||
tuple(so_item_rows),
|
|
||||||
as_dict=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for dn in delivery_notes:
|
for dn in delivery_notes:
|
||||||
@@ -461,15 +499,16 @@ def get_delivery_notes_against_sales_order(item_list):
|
|||||||
|
|
||||||
|
|
||||||
def get_grand_total(filters, doctype):
|
def get_grand_total(filters, doctype):
|
||||||
return frappe.db.sql(
|
return flt(
|
||||||
f""" SELECT
|
frappe.db.get_value(
|
||||||
SUM(`tab{doctype}`.base_grand_total)
|
doctype,
|
||||||
FROM `tab{doctype}`
|
{
|
||||||
WHERE `tab{doctype}`.docstatus = 1
|
"docstatus": 1,
|
||||||
and posting_date between %s and %s
|
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
|
||||||
""",
|
},
|
||||||
(filters.get("from_date"), filters.get("to_date")),
|
"sum(base_grand_total)",
|
||||||
)[0][0] # nosec
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_tax_accounts(
|
def get_tax_accounts(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"calculate_depreciation",
|
"calculate_depreciation",
|
||||||
"column_break_33",
|
"column_break_33",
|
||||||
"opening_accumulated_depreciation",
|
"opening_accumulated_depreciation",
|
||||||
"number_of_depreciations_booked",
|
"opening_number_of_booked_depreciations",
|
||||||
"is_fully_depreciated",
|
"is_fully_depreciated",
|
||||||
"section_break_36",
|
"section_break_36",
|
||||||
"finance_books",
|
"finance_books",
|
||||||
@@ -257,12 +257,6 @@
|
|||||||
"label": "Opening Accumulated Depreciation",
|
"label": "Opening Accumulated Depreciation",
|
||||||
"options": "Company:company:default_currency"
|
"options": "Company:company:default_currency"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "eval:(doc.is_existing_asset)",
|
|
||||||
"fieldname": "number_of_depreciations_booked",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"label": "Number of Depreciations Booked"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset",
|
"collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset",
|
||||||
@@ -546,6 +540,12 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:(doc.is_existing_asset)",
|
||||||
|
"fieldname": "opening_number_of_booked_depreciations",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Opening Number of Booked Depreciations"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 72,
|
"idx": 72,
|
||||||
@@ -589,7 +589,7 @@
|
|||||||
"link_fieldname": "target_asset"
|
"link_fieldname": "target_asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-04-18 16:45:47.306032",
|
"modified": "2024-05-21 13:46:21.066483",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ class Asset(AccountsController):
|
|||||||
maintenance_required: DF.Check
|
maintenance_required: DF.Check
|
||||||
naming_series: DF.Literal["ACC-ASS-.YYYY.-"]
|
naming_series: DF.Literal["ACC-ASS-.YYYY.-"]
|
||||||
next_depreciation_date: DF.Date | None
|
next_depreciation_date: DF.Date | None
|
||||||
number_of_depreciations_booked: DF.Int
|
|
||||||
opening_accumulated_depreciation: DF.Currency
|
opening_accumulated_depreciation: DF.Currency
|
||||||
|
opening_number_of_booked_depreciations: DF.Int
|
||||||
policy_number: DF.Data | None
|
policy_number: DF.Data | None
|
||||||
purchase_amount: DF.Currency
|
purchase_amount: DF.Currency
|
||||||
purchase_date: DF.Date | None
|
purchase_date: DF.Date | None
|
||||||
@@ -145,7 +145,7 @@ class Asset(AccountsController):
|
|||||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||||
).format(asset_depr_schedules_links)
|
).format(asset_depr_schedules_links)
|
||||||
)
|
)
|
||||||
|
self.set_total_booked_depreciations()
|
||||||
self.total_asset_cost = self.gross_purchase_amount
|
self.total_asset_cost = self.gross_purchase_amount
|
||||||
self.status = self.get_status()
|
self.status = self.get_status()
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
if not self.is_existing_asset:
|
if not self.is_existing_asset:
|
||||||
self.opening_accumulated_depreciation = 0
|
self.opening_accumulated_depreciation = 0
|
||||||
self.number_of_depreciations_booked = 0
|
self.opening_number_of_booked_depreciations = 0
|
||||||
else:
|
else:
|
||||||
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||||
@@ -430,15 +430,15 @@ class Asset(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.opening_accumulated_depreciation:
|
if self.opening_accumulated_depreciation:
|
||||||
if not self.number_of_depreciations_booked:
|
if not self.opening_number_of_booked_depreciations:
|
||||||
frappe.throw(_("Please set Number of Depreciations Booked"))
|
frappe.throw(_("Please set Opening Number of Booked Depreciations"))
|
||||||
else:
|
else:
|
||||||
self.number_of_depreciations_booked = 0
|
self.opening_number_of_booked_depreciations = 0
|
||||||
|
|
||||||
if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
|
if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked"
|
"Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
|
||||||
).format(row.idx),
|
).format(row.idx),
|
||||||
title=_("Invalid Schedule"),
|
title=_("Invalid Schedule"),
|
||||||
)
|
)
|
||||||
@@ -459,6 +459,17 @@ class Asset(AccountsController):
|
|||||||
).format(row.idx)
|
).format(row.idx)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_total_booked_depreciations(self):
|
||||||
|
# set value of total number of booked depreciations field
|
||||||
|
for fb_row in self.get("finance_books"):
|
||||||
|
total_number_of_booked_depreciations = self.opening_number_of_booked_depreciations
|
||||||
|
depr_schedule = get_depr_schedule(self.name, "Active", fb_row.finance_book)
|
||||||
|
if depr_schedule:
|
||||||
|
for je in depr_schedule:
|
||||||
|
if je.journal_entry:
|
||||||
|
total_number_of_booked_depreciations += 1
|
||||||
|
fb_row.db_set("total_number_of_booked_depreciations", total_number_of_booked_depreciations)
|
||||||
|
|
||||||
def validate_expected_value_after_useful_life(self):
|
def validate_expected_value_after_useful_life(self):
|
||||||
for row in self.get("finance_books"):
|
for row in self.get("finance_books"):
|
||||||
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
||||||
@@ -1139,6 +1150,8 @@ def create_new_asset_after_split(asset, split_qty):
|
|||||||
|
|
||||||
for row in new_asset.get("finance_books"):
|
for row in new_asset.get("finance_books"):
|
||||||
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
|
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
|
||||||
|
if not current_asset_depr_schedule_doc:
|
||||||
|
continue
|
||||||
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||||
|
|
||||||
new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
|
new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ def _make_journal_entry_for_depreciation(
|
|||||||
|
|
||||||
if not je.meta.get_workflow():
|
if not je.meta.get_workflow():
|
||||||
je.submit()
|
je.submit()
|
||||||
|
asset.reload()
|
||||||
idx = cint(asset_depr_schedule_doc.finance_book_id)
|
idx = cint(asset_depr_schedule_doc.finance_book_id)
|
||||||
row = asset.get("finance_books")[idx - 1]
|
row = asset.get("finance_books")[idx - 1]
|
||||||
row.value_after_depreciation -= depr_schedule.depreciation_amount
|
row.value_after_depreciation -= depr_schedule.depreciation_amount
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ class TestAsset(AssetSetup):
|
|||||||
purchase_date="2020-04-01",
|
purchase_date="2020-04-01",
|
||||||
expected_value_after_useful_life=0,
|
expected_value_after_useful_life=0,
|
||||||
total_number_of_depreciations=5,
|
total_number_of_depreciations=5,
|
||||||
number_of_depreciations_booked=2,
|
opening_number_of_booked_depreciations=2,
|
||||||
frequency_of_depreciation=12,
|
frequency_of_depreciation=12,
|
||||||
depreciation_start_date="2023-03-31",
|
depreciation_start_date="2023-03-31",
|
||||||
opening_accumulated_depreciation=24000,
|
opening_accumulated_depreciation=24000,
|
||||||
@@ -453,7 +453,7 @@ class TestAsset(AssetSetup):
|
|||||||
purchase_date="2020-01-01",
|
purchase_date="2020-01-01",
|
||||||
expected_value_after_useful_life=0,
|
expected_value_after_useful_life=0,
|
||||||
total_number_of_depreciations=6,
|
total_number_of_depreciations=6,
|
||||||
number_of_depreciations_booked=1,
|
opening_number_of_booked_depreciations=1,
|
||||||
frequency_of_depreciation=10,
|
frequency_of_depreciation=10,
|
||||||
depreciation_start_date="2021-01-01",
|
depreciation_start_date="2021-01-01",
|
||||||
opening_accumulated_depreciation=20000,
|
opening_accumulated_depreciation=20000,
|
||||||
@@ -739,7 +739,7 @@ class TestDepreciationMethods(AssetSetup):
|
|||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
available_for_use_date="2030-06-06",
|
available_for_use_date="2030-06-06",
|
||||||
is_existing_asset=1,
|
is_existing_asset=1,
|
||||||
number_of_depreciations_booked=2,
|
opening_number_of_booked_depreciations=2,
|
||||||
opening_accumulated_depreciation=47095.89,
|
opening_accumulated_depreciation=47095.89,
|
||||||
expected_value_after_useful_life=10000,
|
expected_value_after_useful_life=10000,
|
||||||
depreciation_start_date="2032-12-31",
|
depreciation_start_date="2032-12-31",
|
||||||
@@ -789,7 +789,7 @@ class TestDepreciationMethods(AssetSetup):
|
|||||||
available_for_use_date="2030-01-01",
|
available_for_use_date="2030-01-01",
|
||||||
is_existing_asset=1,
|
is_existing_asset=1,
|
||||||
depreciation_method="Double Declining Balance",
|
depreciation_method="Double Declining Balance",
|
||||||
number_of_depreciations_booked=1,
|
opening_number_of_booked_depreciations=1,
|
||||||
opening_accumulated_depreciation=50000,
|
opening_accumulated_depreciation=50000,
|
||||||
expected_value_after_useful_life=10000,
|
expected_value_after_useful_life=10000,
|
||||||
depreciation_start_date="2031-12-31",
|
depreciation_start_date="2031-12-31",
|
||||||
@@ -1123,8 +1123,8 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, asset.save)
|
self.assertRaises(frappe.ValidationError, asset.save)
|
||||||
|
|
||||||
def test_number_of_depreciations_booked(self):
|
def test_opening_booked_depreciations(self):
|
||||||
"""Tests if an error is raised when number_of_depreciations_booked is not specified when opening_accumulated_depreciation is."""
|
"""Tests if an error is raised when opening_number_of_booked_depreciations is not specified when opening_accumulated_depreciation is."""
|
||||||
|
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
item_code="Macbook Pro",
|
item_code="Macbook Pro",
|
||||||
@@ -1140,9 +1140,9 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
self.assertRaises(frappe.ValidationError, asset.save)
|
self.assertRaises(frappe.ValidationError, asset.save)
|
||||||
|
|
||||||
def test_number_of_depreciations(self):
|
def test_number_of_depreciations(self):
|
||||||
"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
|
"""Tests if an error is raised when opening_number_of_booked_depreciations >= total_number_of_depreciations."""
|
||||||
|
|
||||||
# number_of_depreciations_booked > total_number_of_depreciations
|
# opening_number_of_booked_depreciations > total_number_of_depreciations
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
item_code="Macbook Pro",
|
item_code="Macbook Pro",
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
@@ -1151,13 +1151,13 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
expected_value_after_useful_life=10000,
|
expected_value_after_useful_life=10000,
|
||||||
depreciation_start_date="2020-07-01",
|
depreciation_start_date="2020-07-01",
|
||||||
opening_accumulated_depreciation=10000,
|
opening_accumulated_depreciation=10000,
|
||||||
number_of_depreciations_booked=5,
|
opening_number_of_booked_depreciations=5,
|
||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, asset.save)
|
self.assertRaises(frappe.ValidationError, asset.save)
|
||||||
|
|
||||||
# number_of_depreciations_booked = total_number_of_depreciations
|
# opening_number_of_booked_depreciations = total_number_of_depreciations
|
||||||
asset_2 = create_asset(
|
asset_2 = create_asset(
|
||||||
item_code="Macbook Pro",
|
item_code="Macbook Pro",
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
@@ -1166,7 +1166,7 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
expected_value_after_useful_life=10000,
|
expected_value_after_useful_life=10000,
|
||||||
depreciation_start_date="2020-07-01",
|
depreciation_start_date="2020-07-01",
|
||||||
opening_accumulated_depreciation=10000,
|
opening_accumulated_depreciation=10000,
|
||||||
number_of_depreciations_booked=5,
|
opening_number_of_booked_depreciations=5,
|
||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1502,7 +1502,7 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
|
|
||||||
asset = create_asset(calculate_depreciation=1)
|
asset = create_asset(calculate_depreciation=1)
|
||||||
asset.opening_accumulated_depreciation = 2000
|
asset.opening_accumulated_depreciation = 2000
|
||||||
asset.number_of_depreciations_booked = 1
|
asset.opening_number_of_booked_depreciations = 1
|
||||||
|
|
||||||
asset.finance_books[0].expected_value_after_useful_life = 100
|
asset.finance_books[0].expected_value_after_useful_life = 100
|
||||||
asset.save()
|
asset.save()
|
||||||
@@ -1696,7 +1696,7 @@ def create_asset(**args):
|
|||||||
"purchase_date": args.purchase_date or "2015-01-01",
|
"purchase_date": args.purchase_date or "2015-01-01",
|
||||||
"calculate_depreciation": args.calculate_depreciation or 0,
|
"calculate_depreciation": args.calculate_depreciation or 0,
|
||||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||||
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
"opening_number_of_booked_depreciations": args.opening_number_of_booked_depreciations or 0,
|
||||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||||
"purchase_amount": args.purchase_amount or 100000,
|
"purchase_amount": args.purchase_amount or 100000,
|
||||||
"maintenance_required": args.maintenance_required or 0,
|
"maintenance_required": args.maintenance_required or 0,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"column_break_2",
|
"column_break_2",
|
||||||
"gross_purchase_amount",
|
"gross_purchase_amount",
|
||||||
"opening_accumulated_depreciation",
|
"opening_accumulated_depreciation",
|
||||||
"number_of_depreciations_booked",
|
"opening_number_of_booked_depreciations",
|
||||||
"finance_book",
|
"finance_book",
|
||||||
"finance_book_id",
|
"finance_book_id",
|
||||||
"depreciation_details_section",
|
"depreciation_details_section",
|
||||||
@@ -171,10 +171,10 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "number_of_depreciations_booked",
|
"fieldname": "opening_number_of_booked_depreciations",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Number of Depreciations Booked",
|
"label": "Opening Number of Booked Depreciations",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
gross_purchase_amount: DF.Currency
|
gross_purchase_amount: DF.Currency
|
||||||
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
|
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
|
||||||
notes: DF.SmallText | None
|
notes: DF.SmallText | None
|
||||||
number_of_depreciations_booked: DF.Int
|
opening_number_of_booked_depreciations: DF.Int
|
||||||
opening_accumulated_depreciation: DF.Currency
|
opening_accumulated_depreciation: DF.Currency
|
||||||
rate_of_depreciation: DF.Percent
|
rate_of_depreciation: DF.Percent
|
||||||
shift_based: DF.Check
|
shift_based: DF.Check
|
||||||
@@ -161,7 +161,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
return (
|
return (
|
||||||
asset_doc.gross_purchase_amount != self.gross_purchase_amount
|
asset_doc.gross_purchase_amount != self.gross_purchase_amount
|
||||||
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
|
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
|
||||||
or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
|
or asset_doc.opening_number_of_booked_depreciations != self.opening_number_of_booked_depreciations
|
||||||
)
|
)
|
||||||
|
|
||||||
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
|
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
|
||||||
@@ -194,7 +194,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
self.finance_book = row.finance_book
|
self.finance_book = row.finance_book
|
||||||
self.finance_book_id = row.idx
|
self.finance_book_id = row.idx
|
||||||
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
|
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
|
||||||
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
|
self.opening_number_of_booked_depreciations = asset_doc.opening_number_of_booked_depreciations or 0
|
||||||
self.gross_purchase_amount = asset_doc.gross_purchase_amount
|
self.gross_purchase_amount = asset_doc.gross_purchase_amount
|
||||||
self.depreciation_method = row.depreciation_method
|
self.depreciation_method = row.depreciation_method
|
||||||
self.total_number_of_depreciations = row.total_number_of_depreciations
|
self.total_number_of_depreciations = row.total_number_of_depreciations
|
||||||
@@ -263,7 +263,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
row.db_update()
|
row.db_update()
|
||||||
|
|
||||||
final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
|
final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||||
self.number_of_depreciations_booked
|
self.opening_number_of_booked_depreciations
|
||||||
)
|
)
|
||||||
|
|
||||||
has_pro_rata = _check_is_pro_rata(asset_doc, row)
|
has_pro_rata = _check_is_pro_rata(asset_doc, row)
|
||||||
@@ -328,7 +328,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
|
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
|
||||||
from_date = add_months(
|
from_date = add_months(
|
||||||
getdate(asset_doc.available_for_use_date),
|
getdate(asset_doc.available_for_use_date),
|
||||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
|
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||||
)
|
)
|
||||||
if self.depreciation_schedule:
|
if self.depreciation_schedule:
|
||||||
from_date = self.depreciation_schedule[-1].schedule_date
|
from_date = self.depreciation_schedule[-1].schedule_date
|
||||||
@@ -378,13 +378,16 @@ class AssetDepreciationSchedule(Document):
|
|||||||
from_date = get_last_day(
|
from_date = get_last_day(
|
||||||
add_months(
|
add_months(
|
||||||
getdate(asset_doc.available_for_use_date),
|
getdate(asset_doc.available_for_use_date),
|
||||||
((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation),
|
(
|
||||||
|
(self.opening_number_of_booked_depreciations - 1)
|
||||||
|
* row.frequency_of_depreciation
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
from_date = add_months(
|
from_date = add_months(
|
||||||
getdate(add_days(asset_doc.available_for_use_date, -1)),
|
getdate(add_days(asset_doc.available_for_use_date, -1)),
|
||||||
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
|
(self.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||||
)
|
)
|
||||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||||
row,
|
row,
|
||||||
@@ -400,7 +403,8 @@ class AssetDepreciationSchedule(Document):
|
|||||||
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
|
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
|
||||||
asset_doc.to_date = add_months(
|
asset_doc.to_date = add_months(
|
||||||
asset_doc.available_for_use_date,
|
asset_doc.available_for_use_date,
|
||||||
(n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
|
(n + self.opening_number_of_booked_depreciations)
|
||||||
|
* cint(row.frequency_of_depreciation),
|
||||||
)
|
)
|
||||||
|
|
||||||
depreciation_amount_without_pro_rata = depreciation_amount
|
depreciation_amount_without_pro_rata = depreciation_amount
|
||||||
@@ -546,7 +550,7 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
|
|||||||
has_pro_rata = False
|
has_pro_rata = False
|
||||||
|
|
||||||
# if not existing asset, from_date = available_for_use_date
|
# if not existing asset, from_date = available_for_use_date
|
||||||
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
# otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||||
# from_date = 01/01/2022
|
# from_date = 01/01/2022
|
||||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
|
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
|
||||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||||
@@ -567,12 +571,12 @@ def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=Fa
|
|||||||
if wdv_or_dd_non_yearly:
|
if wdv_or_dd_non_yearly:
|
||||||
return add_months(
|
return add_months(
|
||||||
asset_doc.available_for_use_date,
|
asset_doc.available_for_use_date,
|
||||||
(asset_doc.number_of_depreciations_booked * 12),
|
(asset_doc.opening_number_of_booked_depreciations * 12),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return add_months(
|
return add_months(
|
||||||
asset_doc.available_for_use_date,
|
asset_doc.available_for_use_date,
|
||||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
|
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -678,7 +682,7 @@ def get_straight_line_or_manual_depr_amount(
|
|||||||
flt(asset.gross_purchase_amount)
|
flt(asset.gross_purchase_amount)
|
||||||
- flt(asset.opening_accumulated_depreciation)
|
- flt(asset.opening_accumulated_depreciation)
|
||||||
- flt(row.expected_value_after_useful_life)
|
- flt(row.expected_value_after_useful_life)
|
||||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
|
||||||
|
|
||||||
|
|
||||||
def get_daily_prorata_based_straight_line_depr(
|
def get_daily_prorata_based_straight_line_depr(
|
||||||
@@ -704,7 +708,7 @@ def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
|||||||
flt(asset.gross_purchase_amount)
|
flt(asset.gross_purchase_amount)
|
||||||
- flt(asset.opening_accumulated_depreciation)
|
- flt(asset.opening_accumulated_depreciation)
|
||||||
- flt(row.expected_value_after_useful_life)
|
- flt(row.expected_value_after_useful_life)
|
||||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
|
||||||
|
|
||||||
asset_shift_factors_map = get_asset_shift_factors_map()
|
asset_shift_factors_map = get_asset_shift_factors_map()
|
||||||
shift = (
|
shift = (
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import frappe
|
|||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import cstr
|
from frappe.utils import cstr
|
||||||
|
|
||||||
|
from erpnext.assets.doctype.asset.depreciation import (
|
||||||
|
post_depreciation_entries,
|
||||||
|
)
|
||||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||||
get_asset_depr_schedule_doc,
|
get_asset_depr_schedule_doc,
|
||||||
@@ -28,7 +31,7 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
||||||
|
|
||||||
def test_daily_prorata_based_depr_on_sl_methond(self):
|
def test_daily_prorata_based_depr_on_sl_method(self):
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
depreciation_method="Straight Line",
|
depreciation_method="Straight Line",
|
||||||
@@ -160,3 +163,35 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
|||||||
for d in get_depr_schedule(asset.name, "Draft")
|
for d in get_depr_schedule(asset.name, "Draft")
|
||||||
]
|
]
|
||||||
self.assertEqual(schedules, expected_schedules)
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
|
||||||
|
def test_update_total_number_of_booked_depreciations(self):
|
||||||
|
# check if updates total number of booked depreciations when depreciation gets booked
|
||||||
|
asset = create_asset(
|
||||||
|
item_code="Macbook Pro",
|
||||||
|
calculate_depreciation=1,
|
||||||
|
opening_accumulated_depreciation=2000,
|
||||||
|
opening_number_of_booked_depreciations=2,
|
||||||
|
depreciation_method="Straight Line",
|
||||||
|
available_for_use_date="2020-03-01",
|
||||||
|
depreciation_start_date="2020-03-31",
|
||||||
|
frequency_of_depreciation=1,
|
||||||
|
total_number_of_depreciations=24,
|
||||||
|
submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
post_depreciation_entries(date="2021-03-31")
|
||||||
|
asset.reload()
|
||||||
|
"""
|
||||||
|
opening_number_of_booked_depreciations = 2
|
||||||
|
number_of_booked_depreciations till 2021-03-31 = 13
|
||||||
|
total_number_of_booked_depreciations = 15
|
||||||
|
"""
|
||||||
|
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 15)
|
||||||
|
|
||||||
|
# cancel depreciation entry
|
||||||
|
depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
|
||||||
|
|
||||||
|
frappe.get_doc("Journal Entry", depr_entry).cancel()
|
||||||
|
asset.reload()
|
||||||
|
|
||||||
|
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 14)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"finance_book",
|
"finance_book",
|
||||||
"depreciation_method",
|
"depreciation_method",
|
||||||
"total_number_of_depreciations",
|
"total_number_of_depreciations",
|
||||||
|
"total_number_of_booked_depreciations",
|
||||||
"daily_prorata_based",
|
"daily_prorata_based",
|
||||||
"shift_based",
|
"shift_based",
|
||||||
"column_break_5",
|
"column_break_5",
|
||||||
@@ -104,12 +105,19 @@
|
|||||||
"fieldname": "shift_based",
|
"fieldname": "shift_based",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Depreciate based on shifts"
|
"label": "Depreciate based on shifts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "total_number_of_booked_depreciations",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Total Number of Booked Depreciations ",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:34.342264",
|
"modified": "2024-05-21 15:48:20.907250",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Finance Book",
|
"name": "Asset Finance Book",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class AssetFinanceBook(Document):
|
|||||||
rate_of_depreciation: DF.Percent
|
rate_of_depreciation: DF.Percent
|
||||||
salvage_value_percentage: DF.Percent
|
salvage_value_percentage: DF.Percent
|
||||||
shift_based: DF.Check
|
shift_based: DF.Check
|
||||||
|
total_number_of_booked_depreciations: DF.Int
|
||||||
total_number_of_depreciations: DF.Int
|
total_number_of_depreciations: DF.Int
|
||||||
value_after_depreciation: DF.Currency
|
value_after_depreciation: DF.Currency
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ class AssetRepair(AccountsController):
|
|||||||
def calculate_last_schedule_date(self, asset, row, extra_months):
|
def calculate_last_schedule_date(self, asset, row, extra_months):
|
||||||
asset.flags.increase_in_asset_life = True
|
asset.flags.increase_in_asset_life = True
|
||||||
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||||
asset.number_of_depreciations_booked
|
asset.opening_number_of_booked_depreciations
|
||||||
)
|
)
|
||||||
|
|
||||||
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
||||||
@@ -410,7 +410,7 @@ class AssetRepair(AccountsController):
|
|||||||
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
|
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
|
||||||
asset.flags.increase_in_asset_life = True
|
asset.flags.increase_in_asset_life = True
|
||||||
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||||
asset.number_of_depreciations_booked
|
asset.opening_number_of_booked_depreciations
|
||||||
)
|
)
|
||||||
|
|
||||||
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ def prepare_chart_data(data, filters):
|
|||||||
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||||
filters_filter_based_on = "Date Range"
|
filters_filter_based_on = "Date Range"
|
||||||
date_field = "purchase_date"
|
date_field = "purchase_date"
|
||||||
filtered_data = [d for d in data if not d.get(date_field)]
|
filtered_data = [d for d in data if d.get(date_field)]
|
||||||
filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||||
filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
item.idx, item.fg_item
|
item.idx, item.fg_item
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif not frappe.get_value("Item", item.fg_item, "default_bom"):
|
elif not item.bom and not frappe.get_value("Item", item.fg_item, "default_bom"):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Default BOM not found for FG Item {1}").format(
|
_("Row #{0}: Default BOM not found for FG Item {1}").format(
|
||||||
item.idx, item.fg_item
|
item.idx, item.fg_item
|
||||||
@@ -906,12 +906,12 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_doc.populate_items_table()
|
target_doc.populate_items_table()
|
||||||
|
source_doc = frappe.get_doc("Purchase Order", source_name)
|
||||||
|
|
||||||
if target_doc.set_warehouse:
|
if target_doc.set_warehouse:
|
||||||
for item in target_doc.items:
|
for item in target_doc.items:
|
||||||
item.warehouse = target_doc.set_warehouse
|
item.warehouse = target_doc.set_warehouse
|
||||||
else:
|
else:
|
||||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
|
||||||
if source_doc.set_warehouse:
|
if source_doc.set_warehouse:
|
||||||
for item in target_doc.items:
|
for item in target_doc.items:
|
||||||
item.warehouse = source_doc.set_warehouse
|
item.warehouse = source_doc.set_warehouse
|
||||||
@@ -919,6 +919,14 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
|||||||
for idx, item in enumerate(target_doc.items):
|
for idx, item in enumerate(target_doc.items):
|
||||||
item.warehouse = source_doc.items[idx].warehouse
|
item.warehouse = source_doc.items[idx].warehouse
|
||||||
|
|
||||||
|
for idx, item in enumerate(target_doc.items):
|
||||||
|
item.job_card = source_doc.items[idx].job_card
|
||||||
|
if not target_doc.supplier_warehouse:
|
||||||
|
# WIP warehouse is set as Supplier Warehouse in Job Card
|
||||||
|
target_doc.supplier_warehouse = frappe.get_cached_value(
|
||||||
|
"Job Card", item.job_card, "wip_warehouse"
|
||||||
|
)
|
||||||
|
|
||||||
return target_doc
|
return target_doc
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,9 @@
|
|||||||
"production_plan",
|
"production_plan",
|
||||||
"production_plan_item",
|
"production_plan_item",
|
||||||
"production_plan_sub_assembly_item",
|
"production_plan_sub_assembly_item",
|
||||||
"page_break"
|
"page_break",
|
||||||
|
"column_break_pjyo",
|
||||||
|
"job_card"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -909,13 +911,24 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_fyqr",
|
"fieldname": "column_break_fyqr",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_pjyo",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "job_card",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Job Card",
|
||||||
|
"options": "Job Card",
|
||||||
|
"search_index": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:24.979325",
|
"modified": "2024-03-27 13:12:24.979325",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
|||||||
@@ -2,3 +2,10 @@
|
|||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||||
|
|
||||||
|
frappe.query_reports["Purchase Order Trends"]["filters"].push({
|
||||||
|
fieldname: "include_closed_orders",
|
||||||
|
label: __("Include Closed Orders"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 0,
|
||||||
|
});
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
doctype = "Batch"
|
doctype = "Batch"
|
||||||
meta = frappe.get_meta(doctype, cached=True)
|
meta = frappe.get_meta(doctype, cached=True)
|
||||||
searchfields = meta.get_search_fields()
|
searchfields = meta.get_search_fields()
|
||||||
|
page_len = 30
|
||||||
|
|
||||||
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
||||||
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
|
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
|
||||||
@@ -427,6 +428,7 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
|||||||
& (stock_ledger_entry.batch_no.isnotnull())
|
& (stock_ledger_entry.batch_no.isnotnull())
|
||||||
)
|
)
|
||||||
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
||||||
|
.having(Sum(stock_ledger_entry.actual_qty) != 0)
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(page_len)
|
.limit(page_len)
|
||||||
)
|
)
|
||||||
@@ -477,6 +479,7 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
|||||||
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
||||||
)
|
)
|
||||||
.groupby(bundle.batch_no, bundle.warehouse)
|
.groupby(bundle.batch_no, bundle.warehouse)
|
||||||
|
.having(Sum(bundle.qty) != 0)
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(page_len)
|
.limit(page_len)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.utils import flt, format_datetime, get_datetime
|
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||||
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
|||||||
target_doc.rejected_warehouse = ""
|
target_doc.rejected_warehouse = ""
|
||||||
target_doc.warehouse = source_doc.rejected_warehouse
|
target_doc.warehouse = source_doc.rejected_warehouse
|
||||||
target_doc.received_qty = target_doc.qty
|
target_doc.received_qty = target_doc.qty
|
||||||
|
target_doc.return_qty_from_rejected_warehouse = 1
|
||||||
|
|
||||||
elif doctype == "Purchase Invoice":
|
elif doctype == "Purchase Invoice":
|
||||||
returned_qty_map = get_returned_qty_map_for_row(
|
returned_qty_map = get_returned_qty_map_for_row(
|
||||||
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
|||||||
if default_warehouse_for_sales_return:
|
if default_warehouse_for_sales_return:
|
||||||
target_doc.warehouse = default_warehouse_for_sales_return
|
target_doc.warehouse = default_warehouse_for_sales_return
|
||||||
|
|
||||||
if source_doc.item_code:
|
if (
|
||||||
|
(source_doc.serial_no or source_doc.batch_no)
|
||||||
|
and not source_doc.serial_and_batch_bundle
|
||||||
|
and not source_doc.use_serial_batch_fields
|
||||||
|
):
|
||||||
|
target_doc.set("use_serial_batch_fields", 1)
|
||||||
|
|
||||||
|
if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
|
||||||
item_details = frappe.get_cached_value(
|
item_details = frappe.get_cached_value(
|
||||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||||
)
|
)
|
||||||
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
|||||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not target_doc.get("use_serial_batch_fields"):
|
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
|
||||||
if not target_doc.get(qty_field):
|
|
||||||
continue
|
|
||||||
|
|
||||||
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
|
||||||
elif target_doc.get("use_serial_batch_fields"):
|
|
||||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
|
||||||
|
|
||||||
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
|
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
@@ -839,3 +841,243 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
|
|||||||
batches.update(get_batches_from_bundle(ids))
|
batches.update(get_batches_from_bundle(ids))
|
||||||
|
|
||||||
return batches
|
return batches
|
||||||
|
|
||||||
|
|
||||||
|
def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
|
||||||
|
available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||||
|
if not available_dict:
|
||||||
|
frappe.throw(_("No Serial / Batches are available for return"))
|
||||||
|
|
||||||
|
return available_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
|
||||||
|
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||||
|
if not _bundle_ids:
|
||||||
|
return frappe._dict({})
|
||||||
|
|
||||||
|
return get_serial_batches_based_on_bundle(field, _bundle_ids)
|
||||||
|
|
||||||
|
|
||||||
|
def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||||
|
available_dict = frappe._dict({})
|
||||||
|
batch_serial_nos = frappe.get_all(
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
fields=[
|
||||||
|
"`tabSerial and Batch Entry`.`serial_no`",
|
||||||
|
"`tabSerial and Batch Entry`.`batch_no`",
|
||||||
|
"`tabSerial and Batch Entry`.`qty`",
|
||||||
|
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||||
|
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||||
|
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||||
|
],
|
||||||
|
filters=[
|
||||||
|
["Serial and Batch Bundle", "name", "in", _bundle_ids],
|
||||||
|
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||||
|
],
|
||||||
|
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in batch_serial_nos:
|
||||||
|
key = row.voucher_detail_no
|
||||||
|
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||||
|
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||||
|
|
||||||
|
if key not in available_dict:
|
||||||
|
available_dict[key] = frappe._dict(
|
||||||
|
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||||
|
)
|
||||||
|
|
||||||
|
available_dict[key]["qty"] += row.qty
|
||||||
|
|
||||||
|
if row.serial_no:
|
||||||
|
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||||
|
elif row.batch_no:
|
||||||
|
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||||
|
|
||||||
|
return available_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
|
||||||
|
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||||
|
|
||||||
|
pluck_field = "serial_and_batch_bundle"
|
||||||
|
if is_rejected:
|
||||||
|
del filters["serial_and_batch_bundle"]
|
||||||
|
filters["rejected_serial_and_batch_bundle"] = ("is", "set")
|
||||||
|
pluck_field = "rejected_serial_and_batch_bundle"
|
||||||
|
|
||||||
|
_bundle_ids = frappe.get_all(
|
||||||
|
doctype,
|
||||||
|
filters=filters,
|
||||||
|
pluck=pluck_field,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not _bundle_ids:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
del filters["name"]
|
||||||
|
|
||||||
|
filters[field] = ("in", reference_ids)
|
||||||
|
|
||||||
|
if not is_rejected:
|
||||||
|
_bundle_ids.extend(
|
||||||
|
frappe.get_all(
|
||||||
|
doctype,
|
||||||
|
filters=filters,
|
||||||
|
pluck="serial_and_batch_bundle",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fields = [
|
||||||
|
"serial_and_batch_bundle",
|
||||||
|
]
|
||||||
|
|
||||||
|
if is_rejected:
|
||||||
|
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||||
|
|
||||||
|
del filters["rejected_serial_and_batch_bundle"]
|
||||||
|
data = frappe.get_all(
|
||||||
|
doctype,
|
||||||
|
fields=fields,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
if not d.get("serial_and_batch_bundle") and not d.get("rejected_serial_and_batch_bundle"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_rejected:
|
||||||
|
if d.get("return_qty_from_rejected_warehouse"):
|
||||||
|
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||||
|
else:
|
||||||
|
_bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
|
||||||
|
else:
|
||||||
|
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||||
|
|
||||||
|
return _bundle_ids
|
||||||
|
|
||||||
|
|
||||||
|
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||||
|
if not qty_field:
|
||||||
|
qty_field = "qty"
|
||||||
|
|
||||||
|
if not warehouse_field:
|
||||||
|
warehouse_field = "warehouse"
|
||||||
|
|
||||||
|
warehouse = row.get(warehouse_field)
|
||||||
|
qty = abs(row.get(qty_field))
|
||||||
|
|
||||||
|
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
||||||
|
|
||||||
|
if data.serial_nos:
|
||||||
|
available_serial_nos = []
|
||||||
|
for serial_no, sn_qty in data.serial_nos.items():
|
||||||
|
if sn_qty != 0:
|
||||||
|
available_serial_nos.append(serial_no)
|
||||||
|
|
||||||
|
if available_serial_nos:
|
||||||
|
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||||
|
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
||||||
|
|
||||||
|
if len(available_serial_nos) > qty:
|
||||||
|
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||||
|
else:
|
||||||
|
filterd_serial_batch["serial_nos"] = available_serial_nos
|
||||||
|
|
||||||
|
elif data.batches:
|
||||||
|
for batch_no, batch_qty in data.batches.items():
|
||||||
|
if parent_doc.get("is_internal_customer"):
|
||||||
|
batch_qty = batch_qty * -1
|
||||||
|
|
||||||
|
if batch_qty <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||||
|
batch_qty = get_available_batch_qty(
|
||||||
|
parent_doc,
|
||||||
|
batch_no,
|
||||||
|
warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
if batch_qty <= 0:
|
||||||
|
frappe.throw(
|
||||||
|
_("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
|
||||||
|
title=_("Batch Not Available for Return"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if qty <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
if batch_qty > qty:
|
||||||
|
filterd_serial_batch["batches"][batch_no] = qty
|
||||||
|
qty = 0
|
||||||
|
else:
|
||||||
|
filterd_serial_batch["batches"][batch_no] += batch_qty
|
||||||
|
qty -= batch_qty
|
||||||
|
|
||||||
|
return filterd_serial_batch
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_batch_qty(parent_doc, batch_no, warehouse):
|
||||||
|
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||||
|
|
||||||
|
return get_batch_qty(
|
||||||
|
batch_no,
|
||||||
|
warehouse,
|
||||||
|
posting_date=parent_doc.posting_date,
|
||||||
|
posting_time=parent_doc.posting_time,
|
||||||
|
for_stock_levels=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None, qty_field=None):
|
||||||
|
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||||
|
|
||||||
|
type_of_transaction = "Outward"
|
||||||
|
if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
|
||||||
|
type_of_transaction = "Inward"
|
||||||
|
|
||||||
|
if not warehouse_field:
|
||||||
|
warehouse_field = "warehouse"
|
||||||
|
|
||||||
|
if not qty_field:
|
||||||
|
qty_field = "qty"
|
||||||
|
|
||||||
|
warehouse = child_doc.get(warehouse_field)
|
||||||
|
if parent_doc.get("is_internal_customer"):
|
||||||
|
warehouse = child_doc.get("target_warehouse")
|
||||||
|
type_of_transaction = "Outward"
|
||||||
|
|
||||||
|
if not child_doc.get(qty_field):
|
||||||
|
frappe.throw(
|
||||||
|
_("For the {0}, the quantity is required to make the return entry").format(
|
||||||
|
frappe.bold(child_doc.item_code)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
cls_obj = SerialBatchCreation(
|
||||||
|
{
|
||||||
|
"type_of_transaction": type_of_transaction,
|
||||||
|
"item_code": child_doc.item_code,
|
||||||
|
"warehouse": warehouse,
|
||||||
|
"serial_nos": data.get("serial_nos"),
|
||||||
|
"batches": data.get("batches"),
|
||||||
|
"posting_date": parent_doc.posting_date,
|
||||||
|
"posting_time": parent_doc.posting_time,
|
||||||
|
"voucher_type": parent_doc.doctype,
|
||||||
|
"voucher_no": parent_doc.name,
|
||||||
|
"voucher_detail_no": child_doc.name,
|
||||||
|
"qty": child_doc.get(qty_field),
|
||||||
|
"company": parent_doc.company,
|
||||||
|
"do_not_submit": True,
|
||||||
|
}
|
||||||
|
).make_serial_and_batch_bundle()
|
||||||
|
|
||||||
|
return cls_obj.name
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_serial_nos(serial_nos, warehouse):
|
||||||
|
return frappe.get_all(
|
||||||
|
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
|
||||||
|
)
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
|
|||||||
)
|
)
|
||||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import (
|
||||||
|
available_serial_batch_for_return,
|
||||||
|
filter_serial_batches,
|
||||||
|
make_serial_batch_bundle_for_return,
|
||||||
|
)
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||||
get_evaluated_inventory_dimension,
|
get_evaluated_inventory_dimension,
|
||||||
@@ -217,6 +222,132 @@ class StockController(AccountsController):
|
|||||||
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||||
self.create_serial_batch_bundle(bundle_details, row)
|
self.create_serial_batch_bundle(bundle_details, row)
|
||||||
|
|
||||||
|
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
||||||
|
if not self.get("is_return"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not table_name:
|
||||||
|
table_name = "items"
|
||||||
|
|
||||||
|
self.make_bundle_for_non_rejected_qty(table_name)
|
||||||
|
|
||||||
|
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||||
|
self.make_bundle_for_rejected_qty(table_name)
|
||||||
|
|
||||||
|
def make_bundle_for_rejected_qty(self, table_name=None):
|
||||||
|
field, reference_ids = self.get_reference_ids(
|
||||||
|
table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not reference_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
child_doctype = self.doctype + " Item"
|
||||||
|
available_dict = available_serial_batch_for_return(
|
||||||
|
field, child_doctype, reference_ids, is_rejected=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in self.get(table_name):
|
||||||
|
if data := available_dict.get(row.get(field)):
|
||||||
|
qty_field = "rejected_qty"
|
||||||
|
warehouse_field = "rejected_warehouse"
|
||||||
|
if row.get("return_qty_from_rejected_warehouse"):
|
||||||
|
qty_field = "qty"
|
||||||
|
warehouse_field = "warehouse"
|
||||||
|
|
||||||
|
if not data.get("qty"):
|
||||||
|
frappe.throw(
|
||||||
|
_("For the {0}, no stock is available for the return in the warehouse {1}.").format(
|
||||||
|
frappe.bold(row.item_code), row.get(warehouse_field)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = filter_serial_batches(
|
||||||
|
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
|
||||||
|
)
|
||||||
|
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field, qty_field)
|
||||||
|
if row.get("return_qty_from_rejected_warehouse"):
|
||||||
|
row.db_set(
|
||||||
|
{
|
||||||
|
"serial_and_batch_bundle": bundle,
|
||||||
|
"batch_no": "",
|
||||||
|
"serial_no": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
row.db_set(
|
||||||
|
{
|
||||||
|
"rejected_serial_and_batch_bundle": bundle,
|
||||||
|
"batch_no": "",
|
||||||
|
"rejected_serial_no": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_bundle_for_non_rejected_qty(self, table_name):
|
||||||
|
field, reference_ids = self.get_reference_ids(table_name)
|
||||||
|
if not reference_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
child_doctype = self.doctype + " Item"
|
||||||
|
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
|
||||||
|
|
||||||
|
for row in self.get(table_name):
|
||||||
|
if data := available_dict.get(row.get(field)):
|
||||||
|
data = filter_serial_batches(self, data, row)
|
||||||
|
bundle = make_serial_batch_bundle_for_return(data, row, self)
|
||||||
|
row.db_set(
|
||||||
|
{
|
||||||
|
"serial_and_batch_bundle": bundle,
|
||||||
|
"batch_no": "",
|
||||||
|
"serial_no": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||||
|
field = {
|
||||||
|
"Sales Invoice": "sales_invoice_item",
|
||||||
|
"Delivery Note": "dn_detail",
|
||||||
|
"Purchase Receipt": "purchase_receipt_item",
|
||||||
|
"Purchase Invoice": "purchase_invoice_item",
|
||||||
|
"POS Invoice": "pos_invoice_item",
|
||||||
|
}.get(self.doctype)
|
||||||
|
|
||||||
|
if not bundle_field:
|
||||||
|
bundle_field = "serial_and_batch_bundle"
|
||||||
|
|
||||||
|
if not qty_field:
|
||||||
|
qty_field = "qty"
|
||||||
|
|
||||||
|
reference_ids = []
|
||||||
|
|
||||||
|
for row in self.get(table_name):
|
||||||
|
if not self.is_serial_batch_item(row.item_code):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
row.get(field)
|
||||||
|
and (
|
||||||
|
qty_field == "qty"
|
||||||
|
and not row.get("return_qty_from_rejected_warehouse")
|
||||||
|
or qty_field == "rejected_qty"
|
||||||
|
and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
|
||||||
|
)
|
||||||
|
and not row.get("use_serial_batch_fields")
|
||||||
|
and not row.get(bundle_field)
|
||||||
|
):
|
||||||
|
reference_ids.append(row.get(field))
|
||||||
|
|
||||||
|
return field, reference_ids
|
||||||
|
|
||||||
|
@frappe.request_cache
|
||||||
|
def is_serial_batch_item(self, item_code) -> bool:
|
||||||
|
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||||
|
|
||||||
|
if item_details.has_serial_no or item_details.has_batch_no:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
@@ -591,6 +722,9 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
row.db_set("rejected_serial_and_batch_bundle", None)
|
row.db_set("rejected_serial_and_batch_bundle", None)
|
||||||
|
|
||||||
|
if row.get("current_serial_and_batch_bundle"):
|
||||||
|
row.db_set("current_serial_and_batch_bundle", None)
|
||||||
|
|
||||||
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
|
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
|
||||||
if not table_name:
|
if not table_name:
|
||||||
table_name = "items"
|
table_name = "items"
|
||||||
@@ -611,35 +745,16 @@ class StockController(AccountsController):
|
|||||||
def make_package_for_transfer(
|
def make_package_for_transfer(
|
||||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
||||||
):
|
):
|
||||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
|
return make_bundle_for_material_transfer(
|
||||||
|
is_new=self.is_new(),
|
||||||
if not type_of_transaction:
|
docstatus=self.docstatus,
|
||||||
type_of_transaction = "Inward"
|
voucher_type=self.doctype,
|
||||||
|
voucher_no=self.name,
|
||||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
serial_and_batch_bundle=serial_and_batch_bundle,
|
||||||
bundle_doc.warehouse = warehouse
|
warehouse=warehouse,
|
||||||
bundle_doc.type_of_transaction = type_of_transaction
|
type_of_transaction=type_of_transaction,
|
||||||
bundle_doc.voucher_type = self.doctype
|
do_not_submit=do_not_submit,
|
||||||
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
|
)
|
||||||
bundle_doc.is_cancelled = 0
|
|
||||||
|
|
||||||
for row in bundle_doc.entries:
|
|
||||||
row.is_outward = 0
|
|
||||||
row.qty = abs(row.qty)
|
|
||||||
row.stock_value_difference = abs(row.stock_value_difference)
|
|
||||||
if type_of_transaction == "Outward":
|
|
||||||
row.qty *= -1
|
|
||||||
row.stock_value_difference *= row.stock_value_difference
|
|
||||||
row.is_outward = 1
|
|
||||||
|
|
||||||
row.warehouse = warehouse
|
|
||||||
|
|
||||||
bundle_doc.calculate_qty_and_amount()
|
|
||||||
bundle_doc.flags.ignore_permissions = True
|
|
||||||
bundle_doc.flags.ignore_validate = True
|
|
||||||
bundle_doc.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
return bundle_doc.name
|
|
||||||
|
|
||||||
def get_sl_entries(self, d, args):
|
def get_sl_entries(self, d, args):
|
||||||
sl_dict = frappe._dict(
|
sl_dict = frappe._dict(
|
||||||
@@ -1557,3 +1672,38 @@ def create_item_wise_repost_entries(
|
|||||||
repost_entries.append(repost_entry)
|
repost_entries.append(repost_entry)
|
||||||
|
|
||||||
return repost_entries
|
return repost_entries
|
||||||
|
|
||||||
|
|
||||||
|
def make_bundle_for_material_transfer(**kwargs):
|
||||||
|
if isinstance(kwargs, dict):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
|
||||||
|
|
||||||
|
if not kwargs.type_of_transaction:
|
||||||
|
kwargs.type_of_transaction = "Inward"
|
||||||
|
|
||||||
|
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||||
|
bundle_doc.warehouse = kwargs.warehouse
|
||||||
|
bundle_doc.type_of_transaction = kwargs.type_of_transaction
|
||||||
|
bundle_doc.voucher_type = kwargs.voucher_type
|
||||||
|
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
|
||||||
|
bundle_doc.is_cancelled = 0
|
||||||
|
|
||||||
|
for row in bundle_doc.entries:
|
||||||
|
row.is_outward = 0
|
||||||
|
row.qty = abs(row.qty)
|
||||||
|
row.stock_value_difference = abs(row.stock_value_difference)
|
||||||
|
if kwargs.type_of_transaction == "Outward":
|
||||||
|
row.qty *= -1
|
||||||
|
row.stock_value_difference *= row.stock_value_difference
|
||||||
|
row.is_outward = 1
|
||||||
|
|
||||||
|
row.warehouse = kwargs.warehouse
|
||||||
|
|
||||||
|
bundle_doc.calculate_qty_and_amount()
|
||||||
|
bundle_doc.flags.ignore_permissions = True
|
||||||
|
bundle_doc.flags.ignore_validate = True
|
||||||
|
bundle_doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
return bundle_doc.name
|
||||||
|
|||||||
@@ -74,8 +74,10 @@ def get_data(filters, conditions):
|
|||||||
|
|
||||||
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
|
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
|
||||||
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
|
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
|
||||||
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
|
|
||||||
cond += " and t1.status != 'Closed'"
|
if not filters.get("include_closed_orders"):
|
||||||
|
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
|
||||||
|
cond += " and t1.status != 'Closed'"
|
||||||
|
|
||||||
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
|
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
|
||||||
cond += " and t1.quotation_to = 'Customer'"
|
cond += " and t1.quotation_to = 'Customer'"
|
||||||
|
|||||||
@@ -29,13 +29,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
|||||||
|
|
||||||
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
|
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
|
||||||
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
|
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
|
||||||
__("Opportunity"),
|
|
||||||
function () {
|
|
||||||
me.frm.trigger("make_opportunity");
|
|
||||||
},
|
|
||||||
__("Create")
|
|
||||||
);
|
|
||||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||||
if (!doc.__onload.linked_prospects.length) {
|
if (!doc.__onload.linked_prospects.length) {
|
||||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
||||||
@@ -100,6 +94,91 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async make_opportunity() {
|
||||||
|
let existing_prospect = (
|
||||||
|
await frappe.db.get_value(
|
||||||
|
"Prospect Lead",
|
||||||
|
{
|
||||||
|
lead: this.frm.doc.name,
|
||||||
|
},
|
||||||
|
"name",
|
||||||
|
null,
|
||||||
|
"Prospect"
|
||||||
|
)
|
||||||
|
).message.name;
|
||||||
|
|
||||||
|
if (!existing_prospect) {
|
||||||
|
var fields = [
|
||||||
|
{
|
||||||
|
label: "Create Prospect",
|
||||||
|
fieldname: "create_prospect",
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Prospect Name",
|
||||||
|
fieldname: "prospect_name",
|
||||||
|
fieldtype: "Data",
|
||||||
|
default: this.frm.doc.company_name,
|
||||||
|
depends_on: "create_prospect",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
let existing_contact = (
|
||||||
|
await frappe.db.get_value(
|
||||||
|
"Contact",
|
||||||
|
{
|
||||||
|
first_name: this.frm.doc.first_name || this.frm.doc.lead_name,
|
||||||
|
last_name: this.frm.doc.last_name,
|
||||||
|
},
|
||||||
|
"name"
|
||||||
|
)
|
||||||
|
).message.name;
|
||||||
|
|
||||||
|
if (!existing_contact) {
|
||||||
|
fields.push({
|
||||||
|
label: "Create Contact",
|
||||||
|
fieldname: "create_contact",
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: "1",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields) {
|
||||||
|
var d = new frappe.ui.Dialog({
|
||||||
|
title: __("Create Opportunity"),
|
||||||
|
fields: fields,
|
||||||
|
primary_action: function () {
|
||||||
|
var data = d.get_values();
|
||||||
|
frappe.call({
|
||||||
|
method: "create_prospect_and_contact",
|
||||||
|
doc: this.frm.doc,
|
||||||
|
args: {
|
||||||
|
data: data,
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: function (r) {
|
||||||
|
if (!r.exc) {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||||
|
frm: this.frm,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
primary_action_label: __("Create"),
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
} else {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||||
|
frm: this.frm,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
make_prospect() {
|
make_prospect() {
|
||||||
const me = this;
|
const me = this;
|
||||||
frappe.model.with_doctype("Prospect", function () {
|
frappe.model.with_doctype("Prospect", function () {
|
||||||
@@ -151,90 +230,3 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
|||||||
};
|
};
|
||||||
|
|
||||||
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
|
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
|
||||||
|
|
||||||
frappe.ui.form.on("Lead", {
|
|
||||||
make_opportunity: async function (frm) {
|
|
||||||
let existing_prospect = (
|
|
||||||
await frappe.db.get_value(
|
|
||||||
"Prospect Lead",
|
|
||||||
{
|
|
||||||
lead: frm.doc.name,
|
|
||||||
},
|
|
||||||
"name",
|
|
||||||
null,
|
|
||||||
"Prospect"
|
|
||||||
)
|
|
||||||
).message.name;
|
|
||||||
|
|
||||||
if (!existing_prospect) {
|
|
||||||
var fields = [
|
|
||||||
{
|
|
||||||
label: "Create Prospect",
|
|
||||||
fieldname: "create_prospect",
|
|
||||||
fieldtype: "Check",
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Prospect Name",
|
|
||||||
fieldname: "prospect_name",
|
|
||||||
fieldtype: "Data",
|
|
||||||
default: frm.doc.company_name,
|
|
||||||
depends_on: "create_prospect",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
let existing_contact = (
|
|
||||||
await frappe.db.get_value(
|
|
||||||
"Contact",
|
|
||||||
{
|
|
||||||
first_name: frm.doc.first_name || frm.doc.lead_name,
|
|
||||||
last_name: frm.doc.last_name,
|
|
||||||
},
|
|
||||||
"name"
|
|
||||||
)
|
|
||||||
).message.name;
|
|
||||||
|
|
||||||
if (!existing_contact) {
|
|
||||||
fields.push({
|
|
||||||
label: "Create Contact",
|
|
||||||
fieldname: "create_contact",
|
|
||||||
fieldtype: "Check",
|
|
||||||
default: "1",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields) {
|
|
||||||
var d = new frappe.ui.Dialog({
|
|
||||||
title: __("Create Opportunity"),
|
|
||||||
fields: fields,
|
|
||||||
primary_action: function () {
|
|
||||||
var data = d.get_values();
|
|
||||||
frappe.call({
|
|
||||||
method: "create_prospect_and_contact",
|
|
||||||
doc: frm.doc,
|
|
||||||
args: {
|
|
||||||
data: data,
|
|
||||||
},
|
|
||||||
freeze: true,
|
|
||||||
callback: function (r) {
|
|
||||||
if (!r.exc) {
|
|
||||||
frappe.model.open_mapped_doc({
|
|
||||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
|
||||||
frm: frm,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
d.hide();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
primary_action_label: __("Create"),
|
|
||||||
});
|
|
||||||
d.show();
|
|
||||||
} else {
|
|
||||||
frappe.model.open_mapped_doc({
|
|
||||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
|
||||||
frm: frm,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ class Opportunity(TransactionBase, CRMNote):
|
|||||||
self.title = self.customer_name
|
self.title = self.customer_name
|
||||||
|
|
||||||
self.calculate_totals()
|
self.calculate_totals()
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
self.update_prospect()
|
self.update_prospect()
|
||||||
|
|
||||||
def map_fields(self):
|
def map_fields(self):
|
||||||
|
|||||||
@@ -19,6 +19,21 @@ frappe.ui.form.on("BOM", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("bom_no", "operations", function (doc, cdt, cdn) {
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.bom",
|
||||||
|
filters: {
|
||||||
|
currency: frm.doc.currency,
|
||||||
|
company: frm.doc.company,
|
||||||
|
item: row.finished_good,
|
||||||
|
is_active: 1,
|
||||||
|
docstatus: 1,
|
||||||
|
track_semi_finished_goods: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
frm.set_query("source_warehouse", "items", function () {
|
frm.set_query("source_warehouse", "items", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -85,6 +100,27 @@ frappe.ui.form.on("BOM", {
|
|||||||
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
|
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
default_source_warehouse(frm) {
|
||||||
|
if (frm.doc.default_source_warehouse) {
|
||||||
|
frm.doc.operations.forEach((d) => {
|
||||||
|
frappe.model.set_value(
|
||||||
|
d.doctype,
|
||||||
|
d.name,
|
||||||
|
"source_warehouse",
|
||||||
|
frm.doc.default_source_warehouse
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
default_target_warehouse(frm) {
|
||||||
|
if (frm.doc.default_source_warehouse) {
|
||||||
|
frm.doc.operations.forEach((d) => {
|
||||||
|
frappe.model.set_value(d.doctype, d.name, "fg_warehouse", frm.doc.default_target_warehouse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
frm.toggle_enable("item", frm.doc.__islocal);
|
frm.toggle_enable("item", frm.doc.__islocal);
|
||||||
|
|
||||||
@@ -96,22 +132,35 @@ frappe.ui.form.on("BOM", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!frm.is_new() && frm.doc.docstatus < 2) {
|
if (!frm.is_new() && frm.doc.docstatus < 2) {
|
||||||
frm.add_custom_button(__("Update Cost"), function () {
|
frm.add_custom_button(
|
||||||
frm.events.update_cost(frm, true);
|
__("Update Cost"),
|
||||||
});
|
function () {
|
||||||
frm.add_custom_button(__("Browse BOM"), function () {
|
frm.events.update_cost(frm, true);
|
||||||
frappe.route_options = {
|
},
|
||||||
bom: frm.doc.name,
|
__("Actions")
|
||||||
};
|
);
|
||||||
frappe.set_route("Tree", "BOM");
|
|
||||||
});
|
frm.add_custom_button(
|
||||||
|
__("Browse BOM"),
|
||||||
|
function () {
|
||||||
|
frappe.route_options = {
|
||||||
|
bom: frm.doc.name,
|
||||||
|
};
|
||||||
|
frappe.set_route("Tree", "BOM");
|
||||||
|
},
|
||||||
|
__("Actions")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!frm.is_new() && !frm.doc.docstatus == 0) {
|
if (!frm.is_new() && !frm.doc.docstatus == 0) {
|
||||||
frm.add_custom_button(__("New Version"), function () {
|
frm.add_custom_button(
|
||||||
let new_bom = frappe.model.copy_doc(frm.doc);
|
__("New Version"),
|
||||||
frappe.set_route("Form", "BOM", new_bom.name);
|
function () {
|
||||||
});
|
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||||
|
frappe.set_route("Form", "BOM", new_bom.name);
|
||||||
|
},
|
||||||
|
__("Actions")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.docstatus == 1) {
|
if (frm.doc.docstatus == 1) {
|
||||||
@@ -432,6 +481,28 @@ frappe.ui.form.on("BOM", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("BOM Operation", {
|
||||||
|
bom_no(frm, cdt, cdn) {
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
|
||||||
|
if (row.bom_no && row.finished_good) {
|
||||||
|
frappe.call({
|
||||||
|
method: "add_materials_from_bom",
|
||||||
|
doc: frm.doc,
|
||||||
|
args: {
|
||||||
|
finished_good: row.finished_good,
|
||||||
|
bom_no: row.bom_no,
|
||||||
|
operation_row_id: row.idx,
|
||||||
|
qty: row.finished_good_qty,
|
||||||
|
},
|
||||||
|
callback(r) {
|
||||||
|
refresh_field("items");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
erpnext.bom.BomController = class BomController extends erpnext.TransactionController {
|
erpnext.bom.BomController = class BomController extends erpnext.TransactionController {
|
||||||
conversion_rate(doc) {
|
conversion_rate(doc) {
|
||||||
if (this.frm.doc.currency === this.get_company_currency()) {
|
if (this.frm.doc.currency === this.get_company_currency()) {
|
||||||
@@ -801,3 +872,88 @@ function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) {
|
|||||||
__("Set Quantity")
|
__("Set Quantity")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frappe.ui.form.on("BOM Operation", {
|
||||||
|
add_raw_materials(frm, cdt, cdn) {
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
frm.events._prompt_for_raw_materials(frm, row);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("BOM", {
|
||||||
|
_prompt_for_raw_materials(frm, row) {
|
||||||
|
let fields = frm.events.get_fields_for_prompt(frm, row);
|
||||||
|
frm._bom_rm_dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Add Raw Materials"),
|
||||||
|
fields: fields,
|
||||||
|
primary_action_label: __("Add"),
|
||||||
|
primary_action: () => {
|
||||||
|
let values = frm._bom_rm_dialog.get_values();
|
||||||
|
if (values) {
|
||||||
|
frm.events._add_raw_materials(frm, values);
|
||||||
|
frm._bom_rm_dialog.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
frm._bom_rm_dialog.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
get_fields_for_prompt(frm, row) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __("Raw Materials"),
|
||||||
|
fieldname: "items",
|
||||||
|
fieldtype: "Table",
|
||||||
|
reqd: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: __("Item"),
|
||||||
|
fieldname: "item_code",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item",
|
||||||
|
reqd: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
change() {
|
||||||
|
let doc = this.doc;
|
||||||
|
doc.qty = 1.0;
|
||||||
|
this.grid.set_value("qty", 1.0, doc);
|
||||||
|
},
|
||||||
|
get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
name: ["!=", row.finished_good],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Qty"),
|
||||||
|
fieldname: "qty",
|
||||||
|
default: 1.0,
|
||||||
|
fieldtype: "Float",
|
||||||
|
reqd: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "operation_row_id",
|
||||||
|
fieldtype: "Data",
|
||||||
|
hidden: 1,
|
||||||
|
default: row.idx,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
_add_raw_materials(frm, values) {
|
||||||
|
frm.call({
|
||||||
|
method: "add_raw_materials",
|
||||||
|
doc: frm.doc,
|
||||||
|
args: {
|
||||||
|
operation_row_id: values.operation_row_id,
|
||||||
|
items: values.items,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -26,19 +26,22 @@
|
|||||||
"column_break_ivyw",
|
"column_break_ivyw",
|
||||||
"currency",
|
"currency",
|
||||||
"conversion_rate",
|
"conversion_rate",
|
||||||
"materials_section",
|
|
||||||
"items",
|
|
||||||
"section_break_21",
|
|
||||||
"operations_section_section",
|
"operations_section_section",
|
||||||
"with_operations",
|
"with_operations",
|
||||||
|
"track_semi_finished_goods",
|
||||||
"column_break_23",
|
"column_break_23",
|
||||||
"transfer_material_against",
|
"transfer_material_against",
|
||||||
"routing",
|
"routing",
|
||||||
"fg_based_operating_cost",
|
"fg_based_operating_cost",
|
||||||
|
"column_break_joxb",
|
||||||
|
"default_source_warehouse",
|
||||||
|
"default_target_warehouse",
|
||||||
"fg_based_section_section",
|
"fg_based_section_section",
|
||||||
"operating_cost_per_bom_quantity",
|
"operating_cost_per_bom_quantity",
|
||||||
"operations_section",
|
"operations_section",
|
||||||
"operations",
|
"operations",
|
||||||
|
"materials_section",
|
||||||
|
"items",
|
||||||
"scrap_section",
|
"scrap_section",
|
||||||
"scrap_items_section",
|
"scrap_items_section",
|
||||||
"scrap_items",
|
"scrap_items",
|
||||||
@@ -59,8 +62,8 @@
|
|||||||
"base_total_cost",
|
"base_total_cost",
|
||||||
"more_info_tab",
|
"more_info_tab",
|
||||||
"item_name",
|
"item_name",
|
||||||
"description",
|
|
||||||
"column_break_27",
|
"column_break_27",
|
||||||
|
"description",
|
||||||
"has_variants",
|
"has_variants",
|
||||||
"quality_inspection_section_break",
|
"quality_inspection_section_break",
|
||||||
"inspection_required",
|
"inspection_required",
|
||||||
@@ -211,7 +214,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Work Order",
|
"default": "Work Order",
|
||||||
"depends_on": "with_operations",
|
"depends_on": "eval: doc.with_operations === 1 && doc.track_semi_finished_goods === 0",
|
||||||
"fieldname": "transfer_material_against",
|
"fieldname": "transfer_material_against",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Transfer Material Against",
|
"label": "Transfer Material Against",
|
||||||
@@ -406,8 +409,8 @@
|
|||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.__islocal",
|
"depends_on": "eval:!doc.__islocal",
|
||||||
"fieldname": "section_break0",
|
"fieldname": "section_break0",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Materials Required (Exploded)"
|
"label": "Exploded Items"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "exploded_items",
|
"fieldname": "exploded_items",
|
||||||
@@ -485,11 +488,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Operations"
|
"label": "Show Operations"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_21",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"label": "Operations"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_23",
|
"fieldname": "column_break_23",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
@@ -534,6 +532,8 @@
|
|||||||
"show_dashboard": 1
|
"show_dashboard": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "eval:doc.with_operations",
|
||||||
"fieldname": "operations_section_section",
|
"fieldname": "operations_section_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Operations"
|
"label": "Operations"
|
||||||
@@ -591,13 +591,13 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "fg_based_operating_cost",
|
"fieldname": "fg_based_operating_cost",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "FG based Operating Cost"
|
"label": "Finished Goods based Operating Cost"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "fg_based_operating_cost",
|
"depends_on": "fg_based_operating_cost",
|
||||||
"fieldname": "fg_based_section_section",
|
"fieldname": "fg_based_section_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "FG Based Operating Cost Section"
|
"label": "Finished Goods Based Operating Cost"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "fg_based_operating_cost",
|
"depends_on": "fg_based_operating_cost",
|
||||||
@@ -617,7 +617,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "BOM Creator",
|
"options": "BOM Creator",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "bom_creator_item",
|
"fieldname": "bom_creator_item",
|
||||||
@@ -625,11 +626,36 @@
|
|||||||
"label": "BOM Creator Item",
|
"label": "BOM Creator Item",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_oxbz",
|
"fieldname": "column_break_oxbz",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "with_operations",
|
||||||
|
"description": "Users can consume raw materials and add semi-finished goods or final finished goods against the operation using job cards.",
|
||||||
|
"fieldname": "track_semi_finished_goods",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Track Semi Finished Goods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_joxb",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "default_source_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Default Source Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "default_target_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Default Target Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-sitemap",
|
"icon": "fa fa-sitemap",
|
||||||
@@ -637,7 +663,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-02 16:22:47.518411",
|
"modified": "2024-06-03 16:24:47.518411",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM",
|
"name": "BOM",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.core.doctype.version.version import get_diff
|
from frappe.core.doctype.version.version import get_diff
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import cint, cstr, flt, today
|
from frappe.utils import cint, cstr, flt, parse_json, today
|
||||||
from frappe.website.website_generator import WebsiteGenerator
|
from frappe.website.website_generator import WebsiteGenerator
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -125,6 +125,8 @@ class BOM(WebsiteGenerator):
|
|||||||
company: DF.Link
|
company: DF.Link
|
||||||
conversion_rate: DF.Float
|
conversion_rate: DF.Float
|
||||||
currency: DF.Link
|
currency: DF.Link
|
||||||
|
default_source_warehouse: DF.Link | None
|
||||||
|
default_target_warehouse: DF.Link | None
|
||||||
description: DF.SmallText | None
|
description: DF.SmallText | None
|
||||||
exploded_items: DF.Table[BOMExplosionItem]
|
exploded_items: DF.Table[BOMExplosionItem]
|
||||||
fg_based_operating_cost: DF.Check
|
fg_based_operating_cost: DF.Check
|
||||||
@@ -136,6 +138,7 @@ class BOM(WebsiteGenerator):
|
|||||||
item: DF.Link
|
item: DF.Link
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
items: DF.Table[BOMItem]
|
items: DF.Table[BOMItem]
|
||||||
|
track_semi_finished_goods: DF.Check
|
||||||
operating_cost: DF.Currency
|
operating_cost: DF.Currency
|
||||||
operating_cost_per_bom_quantity: DF.Currency
|
operating_cost_per_bom_quantity: DF.Currency
|
||||||
operations: DF.Table[BOMOperation]
|
operations: DF.Table[BOMOperation]
|
||||||
@@ -245,6 +248,7 @@ class BOM(WebsiteGenerator):
|
|||||||
self.clear_inspection()
|
self.clear_inspection()
|
||||||
self.validate_main_item()
|
self.validate_main_item()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
|
self.set_materials_based_on_operation_bom()
|
||||||
self.set_conversion_rate()
|
self.set_conversion_rate()
|
||||||
self.set_plc_conversion_rate()
|
self.set_plc_conversion_rate()
|
||||||
self.validate_uom_is_interger()
|
self.validate_uom_is_interger()
|
||||||
@@ -544,6 +548,9 @@ class BOM(WebsiteGenerator):
|
|||||||
if not self.with_operations:
|
if not self.with_operations:
|
||||||
self.set("operations", [])
|
self.set("operations", [])
|
||||||
|
|
||||||
|
if not self.with_operations and self.track_semi_finished_goods:
|
||||||
|
self.track_semi_finished_goods = 0
|
||||||
|
|
||||||
def clear_inspection(self):
|
def clear_inspection(self):
|
||||||
if not self.inspection_required:
|
if not self.inspection_required:
|
||||||
self.quality_inspection_template = None
|
self.quality_inspection_template = None
|
||||||
@@ -645,6 +652,49 @@ class BOM(WebsiteGenerator):
|
|||||||
if self.name in {d.bom_no for d in self.items}:
|
if self.name in {d.bom_no for d in self.items}:
|
||||||
_throw_error(self.name)
|
_throw_error(self.name)
|
||||||
|
|
||||||
|
def set_materials_based_on_operation_bom(self):
|
||||||
|
if not self.track_semi_finished_goods:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in self.get("operations"):
|
||||||
|
if row.bom_no and row.finished_good:
|
||||||
|
self.add_materials_from_bom(row.finished_good, row.bom_no, row.idx, qty=row.finished_good_qty)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def add_raw_materials(self, operation_row_id, items):
|
||||||
|
if isinstance(items, str):
|
||||||
|
items = parse_json(items)
|
||||||
|
|
||||||
|
for row in items:
|
||||||
|
row = parse_json(row)
|
||||||
|
|
||||||
|
row.update(get_item_details(row.get("item_code")))
|
||||||
|
row.operation_row_id = operation_row_id
|
||||||
|
row.idx = None
|
||||||
|
row.name = None
|
||||||
|
self.append("items", row)
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def add_materials_from_bom(self, finished_good, bom_no, operation_row_id, qty=None):
|
||||||
|
if not frappe.db.exists("BOM", {"item": finished_good, "name": bom_no, "docstatus": 1}):
|
||||||
|
frappe.throw(_("BOM {0} not found for the item {1}").format(bom_no, finished_good))
|
||||||
|
|
||||||
|
if not qty:
|
||||||
|
qty = 1
|
||||||
|
|
||||||
|
for row in self.items:
|
||||||
|
if row.operation_row_id == operation_row_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
bom_items = get_bom_items(bom_no, self.company, qty=qty, fetch_exploded=0)
|
||||||
|
for row in bom_items:
|
||||||
|
row.uom = row.stock_uom
|
||||||
|
row.operation_row_id = operation_row_id
|
||||||
|
row.idx = None
|
||||||
|
self.append("items", row)
|
||||||
|
|
||||||
def traverse_tree(self, bom_list=None):
|
def traverse_tree(self, bom_list=None):
|
||||||
def _get_children(bom_no):
|
def _get_children(bom_no):
|
||||||
children = frappe.cache().hget("bom_children", bom_no)
|
children = frappe.cache().hget("bom_children", bom_no)
|
||||||
@@ -1094,6 +1144,11 @@ def get_bom_items_as_dict(
|
|||||||
):
|
):
|
||||||
item_dict = {}
|
item_dict = {}
|
||||||
|
|
||||||
|
group_by_cond = "group by item_code, stock_uom"
|
||||||
|
if frappe.get_cached_value("BOM", bom, "track_semi_finished_goods"):
|
||||||
|
fetch_exploded = 0
|
||||||
|
group_by_cond = "group by item_code, operation_row_id, stock_uom"
|
||||||
|
|
||||||
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
|
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
|
||||||
query = """select
|
query = """select
|
||||||
bom_item.item_code,
|
bom_item.item_code,
|
||||||
@@ -1122,7 +1177,7 @@ def get_bom_items_as_dict(
|
|||||||
and bom.name = %(bom)s
|
and bom.name = %(bom)s
|
||||||
and item.is_stock_item in (1, {is_stock_item})
|
and item.is_stock_item in (1, {is_stock_item})
|
||||||
{where_conditions}
|
{where_conditions}
|
||||||
group by item_code, stock_uom
|
{group_by_cond}
|
||||||
order by idx"""
|
order by idx"""
|
||||||
|
|
||||||
is_stock_item = 0 if include_non_stock_items else 1
|
is_stock_item = 0 if include_non_stock_items else 1
|
||||||
@@ -1132,6 +1187,7 @@ def get_bom_items_as_dict(
|
|||||||
where_conditions="",
|
where_conditions="",
|
||||||
is_stock_item=is_stock_item,
|
is_stock_item=is_stock_item,
|
||||||
qty_field="stock_qty",
|
qty_field="stock_qty",
|
||||||
|
group_by_cond=group_by_cond,
|
||||||
select_columns=""", bom_item.source_warehouse, bom_item.operation,
|
select_columns=""", bom_item.source_warehouse, bom_item.operation,
|
||||||
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
|
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
|
||||||
(Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""",
|
(Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""",
|
||||||
@@ -1147,6 +1203,7 @@ def get_bom_items_as_dict(
|
|||||||
select_columns=", item.description",
|
select_columns=", item.description",
|
||||||
is_stock_item=is_stock_item,
|
is_stock_item=is_stock_item,
|
||||||
qty_field="stock_qty",
|
qty_field="stock_qty",
|
||||||
|
group_by_cond=group_by_cond,
|
||||||
)
|
)
|
||||||
|
|
||||||
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
|
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
|
||||||
@@ -1158,15 +1215,20 @@ def get_bom_items_as_dict(
|
|||||||
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
|
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
|
||||||
select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
|
select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
|
||||||
bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
|
bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
|
||||||
bom_item.description, bom_item.base_rate as rate """,
|
bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id """,
|
||||||
|
group_by_cond=group_by_cond,
|
||||||
)
|
)
|
||||||
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
|
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.item_code in item_dict:
|
key = item.item_code
|
||||||
item_dict[item.item_code]["qty"] += flt(item.qty)
|
if item.operation_row_id:
|
||||||
|
key = (item.item_code, item.operation_row_id)
|
||||||
|
|
||||||
|
if key in item_dict:
|
||||||
|
item_dict[key]["qty"] += flt(item.qty)
|
||||||
else:
|
else:
|
||||||
item_dict[item.item_code] = item
|
item_dict[key] = item
|
||||||
|
|
||||||
for item, item_details in item_dict.items():
|
for item, item_details in item_dict.items():
|
||||||
for d in [
|
for d in [
|
||||||
|
|||||||
@@ -88,9 +88,77 @@ frappe.ui.form.on("BOM Creator", {
|
|||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: 1.0,
|
default: 1.0,
|
||||||
},
|
},
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
|
{
|
||||||
|
label: __("Track Operations"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
fieldname: "track_operations",
|
||||||
|
onchange: (r) => {
|
||||||
|
let track_operations = dialog.get_value("track_operations");
|
||||||
|
if (r.type === "input" && !track_operations) {
|
||||||
|
dialog.set_value("track_semi_finished_goods", 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ fieldtype: "Column Break" },
|
||||||
|
{
|
||||||
|
label: __("Track Semi Finished Goods"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
fieldname: "track_semi_finished_goods",
|
||||||
|
depends_on: "eval:doc.track_operations",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Section Break",
|
||||||
|
label: __("Final Product Operation"),
|
||||||
|
depends_on: "eval:doc.track_semi_finished_goods",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Operation"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
fieldname: "operation",
|
||||||
|
options: "Operation",
|
||||||
|
default: "Assembly",
|
||||||
|
mandatory_depends_on: "eval:doc.track_semi_finished_goods",
|
||||||
|
depends_on: "eval:doc.track_semi_finished_goods",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Operation Time (in mins)"),
|
||||||
|
fieldtype: "Float",
|
||||||
|
fieldname: "operation_time",
|
||||||
|
mandatory_depends_on: "eval:doc.track_semi_finished_goods",
|
||||||
|
depends_on: "eval:doc.track_semi_finished_goods",
|
||||||
|
},
|
||||||
|
{ fieldtype: "Column Break" },
|
||||||
|
{
|
||||||
|
label: __("Workstation Type"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
fieldname: "workstation_type",
|
||||||
|
options: "Workstation",
|
||||||
|
depends_on: "eval:doc.track_semi_finished_goods",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Workstation"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
fieldname: "workstation",
|
||||||
|
options: "Workstation",
|
||||||
|
depends_on: "eval:doc.track_semi_finished_goods",
|
||||||
|
get_query() {
|
||||||
|
let workstation_type = dialog.get_value("workstation_type");
|
||||||
|
|
||||||
|
if (workstation_type) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
workstation_type: dialog.get_value("workstation_type"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
primary_action_label: __("Create"),
|
primary_action_label: __("Create"),
|
||||||
primary_action: (values) => {
|
primary_action: (values) => {
|
||||||
|
frm.events.validate_dialog_values(frm, values);
|
||||||
|
|
||||||
values.doctype = frm.doc.doctype;
|
values.doctype = frm.doc.doctype;
|
||||||
frappe.db.insert(values).then((doc) => {
|
frappe.db.insert(values).then((doc) => {
|
||||||
frappe.set_route("Form", doc.doctype, doc.name);
|
frappe.set_route("Form", doc.doctype, doc.name);
|
||||||
@@ -102,6 +170,18 @@ frappe.ui.form.on("BOM Creator", {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validate_dialog_values(frm, values) {
|
||||||
|
if (values.track_semi_finished_goods) {
|
||||||
|
if (values.final_operation_time <= 0) {
|
||||||
|
frappe.throw(__("Operation Time must be greater than 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.workstation && !values.workstation_type) {
|
||||||
|
frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
set_queries(frm) {
|
set_queries(frm) {
|
||||||
frm.set_query("bom_no", "items", function (doc, cdt, cdn) {
|
frm.set_query("bom_no", "items", function (doc, cdt, cdn) {
|
||||||
let item = frappe.get_doc(cdt, cdn);
|
let item = frappe.get_doc(cdt, cdn);
|
||||||
@@ -121,6 +201,16 @@ frappe.ui.form.on("BOM Creator", {
|
|||||||
query: "erpnext.controllers.queries.item_query",
|
query: "erpnext.controllers.queries.item_query",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("workstation", (doc) => {
|
||||||
|
if (doc.workstation_type) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
workstation_type: doc.workstation_type,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
|
|||||||
@@ -37,6 +37,23 @@
|
|||||||
"items",
|
"items",
|
||||||
"costing_detail",
|
"costing_detail",
|
||||||
"raw_material_cost",
|
"raw_material_cost",
|
||||||
|
"configuration_section",
|
||||||
|
"track_operations",
|
||||||
|
"column_break_obzr",
|
||||||
|
"track_semi_finished_goods",
|
||||||
|
"final_product_operation_section",
|
||||||
|
"operation",
|
||||||
|
"operation_time",
|
||||||
|
"column_break_xnlu",
|
||||||
|
"workstation_type",
|
||||||
|
"workstation",
|
||||||
|
"final_product_warehouse_section",
|
||||||
|
"skip_material_transfer",
|
||||||
|
"backflush_from_wip_warehouse",
|
||||||
|
"source_warehouse",
|
||||||
|
"column_break_buha",
|
||||||
|
"wip_warehouse",
|
||||||
|
"fg_warehouse",
|
||||||
"remarks_tab",
|
"remarks_tab",
|
||||||
"remarks",
|
"remarks",
|
||||||
"section_break_yixm",
|
"section_break_yixm",
|
||||||
@@ -278,6 +295,104 @@
|
|||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Error Log",
|
"label": "Error Log",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "configuration_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Operation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "track_operations",
|
||||||
|
"fieldname": "track_semi_finished_goods",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Track Semi Finished Goods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_obzr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "track_operations",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Track Operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.track_semi_finished_goods === 1",
|
||||||
|
"fieldname": "final_product_operation_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Final Product Operation & Workstation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xnlu",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "operation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Operation",
|
||||||
|
"options": "Operation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "operation_time",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Operation Time (in mins)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "workstation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Workstation",
|
||||||
|
"options": "Workstation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "workstation_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Workstation Type",
|
||||||
|
"options": "Workstation Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.backflush_from_wip_warehouse",
|
||||||
|
"fieldname": "source_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Source Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
|
||||||
|
"fieldname": "wip_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Work In Progress Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fg_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Finished Good Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.track_semi_finished_goods === 1",
|
||||||
|
"fieldname": "final_product_warehouse_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Final Product Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "skip_material_transfer",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Skip Material Transfer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.skip_material_transfer",
|
||||||
|
"fieldname": "backflush_from_wip_warehouse",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Backflush Materials From WIP Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_buha",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-sitemap",
|
"icon": "fa fa-sitemap",
|
||||||
@@ -288,7 +403,7 @@
|
|||||||
"link_fieldname": "bom_creator"
|
"link_fieldname": "bom_creator"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-04-02 16:30:59.779190",
|
"modified": "2024-05-26 15:47:10.101420",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Creator",
|
"name": "BOM Creator",
|
||||||
|
|||||||
@@ -43,16 +43,20 @@ class BOMCreator(Document):
|
|||||||
from erpnext.manufacturing.doctype.bom_creator_item.bom_creator_item import BOMCreatorItem
|
from erpnext.manufacturing.doctype.bom_creator_item.bom_creator_item import BOMCreatorItem
|
||||||
|
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
|
backflush_from_wip_warehouse: DF.Check
|
||||||
buying_price_list: DF.Link | None
|
buying_price_list: DF.Link | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
conversion_rate: DF.Float
|
conversion_rate: DF.Float
|
||||||
currency: DF.Link
|
currency: DF.Link
|
||||||
default_warehouse: DF.Link | None
|
default_warehouse: DF.Link | None
|
||||||
error_log: DF.Text | None
|
error_log: DF.Text | None
|
||||||
|
fg_warehouse: DF.Link | None
|
||||||
item_code: DF.Link
|
item_code: DF.Link
|
||||||
item_group: DF.Link | None
|
item_group: DF.Link | None
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
items: DF.Table[BOMCreatorItem]
|
items: DF.Table[BOMCreatorItem]
|
||||||
|
operation: DF.Link | None
|
||||||
|
operation_time: DF.Float
|
||||||
plc_conversion_rate: DF.Float
|
plc_conversion_rate: DF.Float
|
||||||
price_list_currency: DF.Link | None
|
price_list_currency: DF.Link | None
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
@@ -61,8 +65,15 @@ class BOMCreator(Document):
|
|||||||
remarks: DF.TextEditor | None
|
remarks: DF.TextEditor | None
|
||||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||||
set_rate_based_on_warehouse: DF.Check
|
set_rate_based_on_warehouse: DF.Check
|
||||||
|
skip_material_transfer: DF.Check
|
||||||
|
source_warehouse: DF.Link | None
|
||||||
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
|
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
|
||||||
|
track_operations: DF.Check
|
||||||
|
track_semi_finished_goods: DF.Check
|
||||||
uom: DF.Link | None
|
uom: DF.Link | None
|
||||||
|
wip_warehouse: DF.Link | None
|
||||||
|
workstation: DF.Link | None
|
||||||
|
workstation_type: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
@@ -236,8 +247,10 @@ class BOMCreator(Document):
|
|||||||
|
|
||||||
self.db_set("status", "In Progress")
|
self.db_set("status", "In Progress")
|
||||||
production_item_wise_rm = OrderedDict({})
|
production_item_wise_rm = OrderedDict({})
|
||||||
|
|
||||||
|
final_product = (self.item_code, self.name)
|
||||||
production_item_wise_rm.setdefault(
|
production_item_wise_rm.setdefault(
|
||||||
(self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
|
final_product, frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
|
||||||
)
|
)
|
||||||
|
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
@@ -257,9 +270,15 @@ class BOMCreator(Document):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
for d in reverse_tree:
|
for d in reverse_tree:
|
||||||
|
if self.track_operations and self.track_semi_finished_goods and final_product == d:
|
||||||
|
continue
|
||||||
|
|
||||||
fg_item_data = production_item_wise_rm.get(d).fg_item_data
|
fg_item_data = production_item_wise_rm.get(d).fg_item_data
|
||||||
self.create_bom(fg_item_data, production_item_wise_rm)
|
self.create_bom(fg_item_data, production_item_wise_rm)
|
||||||
|
|
||||||
|
if self.track_operations and self.track_semi_finished_goods:
|
||||||
|
self.make_bom_for_final_product(production_item_wise_rm)
|
||||||
|
|
||||||
frappe.msgprint(_("BOMs created successfully"))
|
frappe.msgprint(_("BOMs created successfully"))
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback = frappe.get_traceback(with_context=True)
|
traceback = frappe.get_traceback(with_context=True)
|
||||||
@@ -272,6 +291,81 @@ class BOMCreator(Document):
|
|||||||
|
|
||||||
frappe.msgprint(_("BOMs creation failed"))
|
frappe.msgprint(_("BOMs creation failed"))
|
||||||
|
|
||||||
|
def make_bom_for_final_product(self, production_item_wise_rm):
|
||||||
|
bom = frappe.new_doc("BOM")
|
||||||
|
bom.update(
|
||||||
|
{
|
||||||
|
"item": self.item_code,
|
||||||
|
"bom_type": "Production",
|
||||||
|
"quantity": self.qty,
|
||||||
|
"allow_alternative_item": 1,
|
||||||
|
"bom_creator": self.name,
|
||||||
|
"bom_creator_item": self.name,
|
||||||
|
"rm_cost_as_per": "Manual",
|
||||||
|
"with_operations": 1,
|
||||||
|
"track_semi_finished_goods": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for field in BOM_FIELDS:
|
||||||
|
if self.get(field):
|
||||||
|
bom.set(field, self.get(field))
|
||||||
|
|
||||||
|
for item in self.items:
|
||||||
|
if not item.is_expandable or not item.operation:
|
||||||
|
continue
|
||||||
|
|
||||||
|
bom.append(
|
||||||
|
"operations",
|
||||||
|
{
|
||||||
|
"operation": item.operation,
|
||||||
|
"workstation": item.workstation,
|
||||||
|
"source_warehouse": item.source_warehouse,
|
||||||
|
"wip_warehouse": item.wip_warehouse,
|
||||||
|
"fg_warehouse": item.fg_warehouse,
|
||||||
|
"finished_good": item.item_code,
|
||||||
|
"finished_good_qty": item.qty,
|
||||||
|
"bom_no": production_item_wise_rm[(item.item_code, item.name)].bom_no,
|
||||||
|
"workstation_type": item.workstation_type,
|
||||||
|
"time_in_mins": item.operation_time,
|
||||||
|
"is_subcontracted": item.is_subcontracted,
|
||||||
|
"skip_material_transfer": item.skip_material_transfer,
|
||||||
|
"backflush_from_wip_warehouse": item.backflush_from_wip_warehouse,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
operation_row = bom.append(
|
||||||
|
"operations",
|
||||||
|
{
|
||||||
|
"operation": self.operation,
|
||||||
|
"time_in_mins": self.operation_time,
|
||||||
|
"workstation": self.workstation,
|
||||||
|
"workstation_type": self.workstation_type,
|
||||||
|
"finished_good": self.item_code,
|
||||||
|
"finished_good_qty": self.qty,
|
||||||
|
"source_warehouse": self.source_warehouse,
|
||||||
|
"wip_warehouse": self.wip_warehouse,
|
||||||
|
"fg_warehouse": self.fg_warehouse,
|
||||||
|
"skip_material_transfer": self.skip_material_transfer,
|
||||||
|
"backflush_from_wip_warehouse": self.backflush_from_wip_warehouse,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
final_product = (self.item_code, self.name)
|
||||||
|
items = production_item_wise_rm.get(final_product).get("items")
|
||||||
|
|
||||||
|
bom.set_materials_based_on_operation_bom()
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
item_args = {"operation_row_id": operation_row.idx}
|
||||||
|
for field in BOM_ITEM_FIELDS:
|
||||||
|
item_args[field] = item.get(field)
|
||||||
|
|
||||||
|
bom.append("items", item_args)
|
||||||
|
|
||||||
|
bom.save(ignore_permissions=True)
|
||||||
|
bom.submit()
|
||||||
|
|
||||||
def create_bom(self, row, production_item_wise_rm):
|
def create_bom(self, row, production_item_wise_rm):
|
||||||
bom_creator_item = row.name if row.name != self.name else ""
|
bom_creator_item = row.name if row.name != self.name else ""
|
||||||
if frappe.db.exists(
|
if frappe.db.exists(
|
||||||
@@ -297,6 +391,24 @@ class BOMCreator(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.track_operations and not self.track_semi_finished_goods:
|
||||||
|
if row.item_code == self.item_code:
|
||||||
|
bom.with_operations = 1
|
||||||
|
bom.transfer_material_against = "Work Order"
|
||||||
|
for item in self.items:
|
||||||
|
if not item.operation:
|
||||||
|
continue
|
||||||
|
|
||||||
|
bom.append(
|
||||||
|
"operations",
|
||||||
|
{
|
||||||
|
"operation": item.operation,
|
||||||
|
"workstation_type": item.workstation_type,
|
||||||
|
"workstation": item.workstation,
|
||||||
|
"time_in_mins": item.operation_time,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
for field in BOM_FIELDS:
|
for field in BOM_FIELDS:
|
||||||
if self.get(field):
|
if self.get(field):
|
||||||
bom.set(field, self.get(field))
|
bom.set(field, self.get(field))
|
||||||
@@ -352,6 +464,16 @@ def get_children(doctype=None, parent=None, **kwargs):
|
|||||||
"uom",
|
"uom",
|
||||||
"rate",
|
"rate",
|
||||||
"amount",
|
"amount",
|
||||||
|
"workstation_type",
|
||||||
|
"operation",
|
||||||
|
"operation_time",
|
||||||
|
"is_subcontracted",
|
||||||
|
"workstation",
|
||||||
|
"source_warehouse",
|
||||||
|
"wip_warehouse",
|
||||||
|
"fg_warehouse",
|
||||||
|
"skip_material_transfer",
|
||||||
|
"backflush_from_wip_warehouse",
|
||||||
]
|
]
|
||||||
|
|
||||||
query_filters = {
|
query_filters = {
|
||||||
@@ -365,6 +487,12 @@ def get_children(doctype=None, parent=None, **kwargs):
|
|||||||
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
|
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
|
||||||
|
|
||||||
|
|
||||||
|
def get_parent_row_no(doc, name):
|
||||||
|
for row in doc.items:
|
||||||
|
if row.name == name:
|
||||||
|
return row.idx
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_item(**kwargs):
|
def add_item(**kwargs):
|
||||||
if isinstance(kwargs, str):
|
if isinstance(kwargs, str):
|
||||||
@@ -375,6 +503,11 @@ def add_item(**kwargs):
|
|||||||
|
|
||||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||||
item_info = get_item_details(kwargs.item_code)
|
item_info = get_item_details(kwargs.item_code)
|
||||||
|
|
||||||
|
parent_row_no = ""
|
||||||
|
if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id:
|
||||||
|
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||||
|
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
"uom": item_info.stock_uom,
|
"uom": item_info.stock_uom,
|
||||||
@@ -383,6 +516,9 @@ def add_item(**kwargs):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if parent_row_no:
|
||||||
|
kwargs.update({"parent_row_no": parent_row_no})
|
||||||
|
|
||||||
doc.append("items", kwargs)
|
doc.append("items", kwargs)
|
||||||
doc.save()
|
doc.save()
|
||||||
|
|
||||||
@@ -402,6 +538,7 @@ def add_sub_assembly(**kwargs):
|
|||||||
|
|
||||||
name = kwargs.fg_reference_id
|
name = kwargs.fg_reference_id
|
||||||
parent_row_no = ""
|
parent_row_no = ""
|
||||||
|
|
||||||
if not kwargs.convert_to_sub_assembly:
|
if not kwargs.convert_to_sub_assembly:
|
||||||
item_info = get_item_details(bom_item.item_code)
|
item_info = get_item_details(bom_item.item_code)
|
||||||
item_row = doc.append(
|
item_row = doc.append(
|
||||||
@@ -417,6 +554,15 @@ def add_sub_assembly(**kwargs):
|
|||||||
"do_not_explode": 1,
|
"do_not_explode": 1,
|
||||||
"is_expandable": 1,
|
"is_expandable": 1,
|
||||||
"stock_uom": item_info.stock_uom,
|
"stock_uom": item_info.stock_uom,
|
||||||
|
"operation": bom_item.operation,
|
||||||
|
"workstation_type": bom_item.workstation_type,
|
||||||
|
"operation_time": bom_item.operation_time,
|
||||||
|
"is_subcontracted": bom_item.is_subcontracted,
|
||||||
|
"workstation": bom_item.workstation,
|
||||||
|
"source_warehouse": bom_item.source_warehouse,
|
||||||
|
"wip_warehouse": bom_item.wip_warehouse,
|
||||||
|
"fg_warehouse": bom_item.fg_warehouse,
|
||||||
|
"skip_material_transfer": bom_item.skip_material_transfer,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -426,6 +572,20 @@ def add_sub_assembly(**kwargs):
|
|||||||
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
|
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
|
||||||
if parent_row_no:
|
if parent_row_no:
|
||||||
parent_row_no = parent_row_no[0]
|
parent_row_no = parent_row_no[0]
|
||||||
|
doc.items[parent_row_no - 1].update(
|
||||||
|
{
|
||||||
|
"operation": bom_item.operation,
|
||||||
|
"workstation_type": bom_item.workstation_type,
|
||||||
|
"operation_time": bom_item.operation_time,
|
||||||
|
"is_subcontracted": bom_item.is_subcontracted,
|
||||||
|
"workstation": bom_item.workstation,
|
||||||
|
"source_warehouse": bom_item.source_warehouse,
|
||||||
|
"wip_warehouse": bom_item.wip_warehouse,
|
||||||
|
"fg_warehouse": bom_item.fg_warehouse,
|
||||||
|
"skip_material_transfer": bom_item.skip_material_transfer,
|
||||||
|
"backflush_from_wip_warehouse": bom_item.backflush_from_wip_warehouse,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
for row in bom_item.get("items"):
|
for row in bom_item.get("items"):
|
||||||
row = frappe._dict(row)
|
row = frappe._dict(row)
|
||||||
@@ -482,10 +642,16 @@ def delete_node(**kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def edit_qty(doctype, docname, qty, parent):
|
def edit_bom_creator(doctype, docname, data, parent):
|
||||||
frappe.db.set_value(doctype, docname, "qty", qty)
|
if isinstance(data, str):
|
||||||
|
data = frappe.parse_json(data)
|
||||||
|
|
||||||
|
frappe.db.set_value(doctype, docname, data)
|
||||||
|
|
||||||
doc = frappe.get_doc("BOM Creator", parent)
|
doc = frappe.get_doc("BOM Creator", parent)
|
||||||
doc.set_rate_for_items()
|
doc.set_rate_for_items()
|
||||||
doc.save()
|
doc.save()
|
||||||
|
|
||||||
|
frappe.msgprint(_("Updated successfully"), alert=True)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|||||||
@@ -11,10 +11,23 @@
|
|||||||
"item_group",
|
"item_group",
|
||||||
"column_break_f63f",
|
"column_break_f63f",
|
||||||
"fg_item",
|
"fg_item",
|
||||||
"source_warehouse",
|
|
||||||
"is_expandable",
|
"is_expandable",
|
||||||
"sourced_by_supplier",
|
"sourced_by_supplier",
|
||||||
"bom_created",
|
"bom_created",
|
||||||
|
"is_subcontracted",
|
||||||
|
"operation_section",
|
||||||
|
"operation",
|
||||||
|
"operation_time",
|
||||||
|
"column_break_cbnk",
|
||||||
|
"workstation_type",
|
||||||
|
"workstation",
|
||||||
|
"warehouse_section",
|
||||||
|
"skip_material_transfer",
|
||||||
|
"backflush_from_wip_warehouse",
|
||||||
|
"source_warehouse",
|
||||||
|
"column_break_xutc",
|
||||||
|
"wip_warehouse",
|
||||||
|
"fg_warehouse",
|
||||||
"description_section",
|
"description_section",
|
||||||
"description",
|
"description",
|
||||||
"quantity_and_rate_section",
|
"quantity_and_rate_section",
|
||||||
@@ -70,21 +83,23 @@
|
|||||||
"fieldname": "fg_item",
|
"fieldname": "fg_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "FG Item",
|
"label": "Finished Goods Item",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse",
|
||||||
"fieldname": "source_warehouse",
|
"fieldname": "source_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Source Warehouse",
|
"label": "Source Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"columns": 1,
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "is_expandable",
|
"fieldname": "is_expandable",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Is Expandable",
|
"label": "Is Expandable",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -203,7 +218,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "fg_reference_id",
|
"fieldname": "fg_reference_id",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "FG Reference",
|
"label": "Finished Goods Reference",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -225,12 +240,87 @@
|
|||||||
"label": "BOM Created",
|
"label": "BOM Created",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "operation_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Operation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "operation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Operation",
|
||||||
|
"options": "Operation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_cbnk",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "workstation_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Workstation Type",
|
||||||
|
"options": "Workstation Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "In Mins",
|
||||||
|
"fieldname": "operation_time",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Operation Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "workstation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Workstation",
|
||||||
|
"options": "Workstation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
|
||||||
|
"fieldname": "wip_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Work In Progress Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xutc",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fg_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Finished Good Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "skip_material_transfer",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Skip Material Transfer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.skip_material_transfer",
|
||||||
|
"fieldname": "backflush_from_wip_warehouse",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Backflush Materials From WIP Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_subcontracted",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Subcontracted",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:40.764747",
|
"modified": "2024-06-03 18:45:24.339532",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Creator Item",
|
"name": "BOM Creator Item",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class BOMCreatorItem(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
amount: DF.Currency
|
amount: DF.Currency
|
||||||
|
backflush_from_wip_warehouse: DF.Check
|
||||||
base_amount: DF.Currency
|
base_amount: DF.Currency
|
||||||
base_rate: DF.Currency
|
base_rate: DF.Currency
|
||||||
bom_created: DF.Check
|
bom_created: DF.Check
|
||||||
@@ -23,22 +24,30 @@ class BOMCreatorItem(Document):
|
|||||||
do_not_explode: DF.Check
|
do_not_explode: DF.Check
|
||||||
fg_item: DF.Link
|
fg_item: DF.Link
|
||||||
fg_reference_id: DF.Data | None
|
fg_reference_id: DF.Data | None
|
||||||
|
fg_warehouse: DF.Link | None
|
||||||
instruction: DF.SmallText | None
|
instruction: DF.SmallText | None
|
||||||
is_expandable: DF.Check
|
is_expandable: DF.Check
|
||||||
|
is_subcontracted: DF.Check
|
||||||
item_code: DF.Link
|
item_code: DF.Link
|
||||||
item_group: DF.Link | None
|
item_group: DF.Link | None
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
|
operation: DF.Link | None
|
||||||
|
operation_time: DF.Int
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parent_row_no: DF.Data | None
|
parent_row_no: DF.Data | None
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
qty: DF.Float
|
qty: DF.Float
|
||||||
rate: DF.Currency
|
rate: DF.Currency
|
||||||
|
skip_material_transfer: DF.Check
|
||||||
source_warehouse: DF.Link | None
|
source_warehouse: DF.Link | None
|
||||||
sourced_by_supplier: DF.Check
|
sourced_by_supplier: DF.Check
|
||||||
stock_qty: DF.Float
|
stock_qty: DF.Float
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
uom: DF.Link | None
|
uom: DF.Link | None
|
||||||
|
wip_warehouse: DF.Link | None
|
||||||
|
workstation: DF.Link | None
|
||||||
|
workstation_type: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"item_code",
|
"item_code",
|
||||||
"item_name",
|
"item_name",
|
||||||
"operation",
|
"operation",
|
||||||
|
"operation_row_id",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"do_not_explode",
|
"do_not_explode",
|
||||||
"bom_no",
|
"bom_no",
|
||||||
@@ -293,13 +294,19 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Stock Item",
|
"label": "Is Stock Item",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:parent.track_semi_finished_goods ==1",
|
||||||
|
"fieldname": "operation_row_id",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Operation ID"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:41.079752",
|
"modified": "2024-03-27 13:08:41.079752",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Item",
|
"name": "BOM Item",
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ class BOMItem(Document):
|
|||||||
has_variants: DF.Check
|
has_variants: DF.Check
|
||||||
image: DF.Attach | None
|
image: DF.Attach | None
|
||||||
include_item_in_manufacturing: DF.Check
|
include_item_in_manufacturing: DF.Check
|
||||||
|
is_stock_item: DF.Check
|
||||||
item_code: DF.Link
|
item_code: DF.Link
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
operation: DF.Link | None
|
operation: DF.Link | None
|
||||||
|
operation_row_id: DF.Int
|
||||||
original_item: DF.Link | None
|
original_item: DF.Link | None
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
|
|||||||
@@ -6,24 +6,36 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"sequence_id",
|
|
||||||
"operation",
|
"operation",
|
||||||
|
"sequence_id",
|
||||||
|
"finished_good",
|
||||||
|
"finished_good_qty",
|
||||||
|
"bom_no",
|
||||||
|
"add_raw_materials",
|
||||||
"col_break1",
|
"col_break1",
|
||||||
"workstation_type",
|
"workstation_type",
|
||||||
"workstation",
|
"workstation",
|
||||||
"time_in_mins",
|
"time_in_mins",
|
||||||
"fixed_time",
|
"fixed_time",
|
||||||
|
"is_subcontracted",
|
||||||
|
"is_final_finished_good",
|
||||||
|
"set_cost_based_on_bom_qty",
|
||||||
|
"warehouse_section",
|
||||||
|
"skip_material_transfer",
|
||||||
|
"backflush_from_wip_warehouse",
|
||||||
|
"source_warehouse",
|
||||||
|
"column_break_lbhy",
|
||||||
|
"wip_warehouse",
|
||||||
|
"fg_warehouse",
|
||||||
"costing_section",
|
"costing_section",
|
||||||
"hour_rate",
|
"hour_rate",
|
||||||
"base_hour_rate",
|
"base_hour_rate",
|
||||||
"column_break_9",
|
|
||||||
"operating_cost",
|
|
||||||
"base_operating_cost",
|
|
||||||
"column_break_11",
|
|
||||||
"batch_size",
|
"batch_size",
|
||||||
"set_cost_based_on_bom_qty",
|
"column_break_11",
|
||||||
"cost_per_unit",
|
"cost_per_unit",
|
||||||
"base_cost_per_unit",
|
"base_cost_per_unit",
|
||||||
|
"operating_cost",
|
||||||
|
"base_operating_cost",
|
||||||
"more_information_section",
|
"more_information_section",
|
||||||
"description",
|
"description",
|
||||||
"column_break_18",
|
"column_break_18",
|
||||||
@@ -71,13 +83,14 @@
|
|||||||
"precision": "2"
|
"precision": "2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"columns": 1,
|
||||||
"description": "In minutes",
|
"description": "In minutes",
|
||||||
"fetch_from": "operation.total_operation_time",
|
"fetch_from": "operation.total_operation_time",
|
||||||
"fetch_if_empty": 1,
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "time_in_mins",
|
"fieldname": "time_in_mins",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Operation Time ",
|
"label": "Operation Time",
|
||||||
"oldfieldname": "time_in_mins",
|
"oldfieldname": "time_in_mins",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -87,7 +100,6 @@
|
|||||||
"description": "Operation time does not depend on quantity to produce",
|
"description": "Operation time does not depend on quantity to produce",
|
||||||
"fieldname": "fixed_time",
|
"fieldname": "fixed_time",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Fixed Time"
|
"label": "Fixed Time"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -172,10 +184,6 @@
|
|||||||
"fieldname": "column_break_18",
|
"fieldname": "column_break_18",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_9",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "set_cost_based_on_bom_qty",
|
"fieldname": "set_cost_based_on_bom_qty",
|
||||||
@@ -183,18 +191,106 @@
|
|||||||
"label": "Set Operating Cost Based On BOM Quantity"
|
"label": "Set Operating Cost Based On BOM Quantity"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"columns": 1,
|
||||||
"fieldname": "workstation_type",
|
"fieldname": "workstation_type",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Workstation Type",
|
"label": "Workstation Type",
|
||||||
"options": "Workstation Type"
|
"options": "Workstation Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "finished_good",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Finished Goods / Semi Finished Goods Item",
|
||||||
|
"options": "Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "bom_no",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "BOM No",
|
||||||
|
"options": "BOM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "finished_good_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Finished Goods Qty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_final_finished_good",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Final Finished Good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
|
||||||
|
"fieldname": "wip_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "WIP Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_lbhy",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "fg_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Finished Goods Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse",
|
||||||
|
"fieldname": "source_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Source Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_subcontracted",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Subcontracted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.bom_no",
|
||||||
|
"fieldname": "add_raw_materials",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Add Raw Materials"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "skip_material_transfer",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": " Skip Material Transfer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.skip_material_transfer",
|
||||||
|
"fieldname": "backflush_from_wip_warehouse",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Backflush Materials From WIP Warehouse"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:41.248462",
|
"modified": "2024-06-03 15:46:49.404875",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Operation",
|
"name": "BOM Operation",
|
||||||
@@ -203,4 +299,4 @@
|
|||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,22 @@ class BOMOperation(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
|
backflush_from_wip_warehouse: DF.Check
|
||||||
base_cost_per_unit: DF.Float
|
base_cost_per_unit: DF.Float
|
||||||
base_hour_rate: DF.Currency
|
base_hour_rate: DF.Currency
|
||||||
base_operating_cost: DF.Currency
|
base_operating_cost: DF.Currency
|
||||||
batch_size: DF.Int
|
batch_size: DF.Int
|
||||||
|
bom_no: DF.Link | None
|
||||||
cost_per_unit: DF.Float
|
cost_per_unit: DF.Float
|
||||||
description: DF.TextEditor | None
|
description: DF.TextEditor | None
|
||||||
|
fg_warehouse: DF.Link | None
|
||||||
|
finished_good: DF.Link | None
|
||||||
|
finished_good_qty: DF.Float
|
||||||
fixed_time: DF.Check
|
fixed_time: DF.Check
|
||||||
hour_rate: DF.Currency
|
hour_rate: DF.Currency
|
||||||
image: DF.Attach | None
|
image: DF.Attach | None
|
||||||
|
is_final_finished_good: DF.Check
|
||||||
|
is_subcontracted: DF.Check
|
||||||
operating_cost: DF.Currency
|
operating_cost: DF.Currency
|
||||||
operation: DF.Link
|
operation: DF.Link
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
@@ -30,7 +37,10 @@ class BOMOperation(Document):
|
|||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
sequence_id: DF.Int
|
sequence_id: DF.Int
|
||||||
set_cost_based_on_bom_qty: DF.Check
|
set_cost_based_on_bom_qty: DF.Check
|
||||||
|
skip_material_transfer: DF.Check
|
||||||
|
source_warehouse: DF.Link | None
|
||||||
time_in_mins: DF.Float
|
time_in_mins: DF.Float
|
||||||
|
wip_warehouse: DF.Link | None
|
||||||
workstation: DF.Link | None
|
workstation: DF.Link | None
|
||||||
workstation_type: DF.Link | None
|
workstation_type: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -32,21 +32,61 @@ frappe.ui.form.on("Job Card", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
make_fields_read_only(frm) {
|
||||||
|
if (frm.doc.docstatus === 1) {
|
||||||
|
frm.set_df_property("employee", "read_only", 1);
|
||||||
|
frm.set_df_property("time_logs", "read_only", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frm.doc.is_subcontracted) {
|
||||||
|
frm.set_df_property("wip_warehouse", "label", __("Supplier Warehouse"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setup_stock_entry(frm) {
|
||||||
|
if (
|
||||||
|
frm.doc.finished_good &&
|
||||||
|
frm.doc.docstatus === 1 &&
|
||||||
|
!frm.doc.is_subcontracted &&
|
||||||
|
flt(frm.doc.for_quantity) + flt(frm.doc.process_loss_qty) > flt(frm.doc.manufactured_qty)
|
||||||
|
) {
|
||||||
|
frm.add_custom_button(__("Make Stock Entry"), () => {
|
||||||
|
frm.call({
|
||||||
|
method: "make_stock_entry_for_semi_fg_item",
|
||||||
|
args: {
|
||||||
|
auto_submit: 1,
|
||||||
|
},
|
||||||
|
doc: frm.doc,
|
||||||
|
freeze: true,
|
||||||
|
callback() {
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).addClass("btn-primary");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
frappe.flags.pause_job = 0;
|
frm.trigger("setup_stock_entry");
|
||||||
frappe.flags.resume_job = 0;
|
|
||||||
let has_items = frm.doc.items && frm.doc.items.length;
|
let has_items = frm.doc.items && frm.doc.items.length;
|
||||||
|
frm.trigger("make_fields_read_only");
|
||||||
|
|
||||||
if (!frm.is_new() && frm.doc.__onload.work_order_closed) {
|
if (!frm.is_new() && frm.doc.__onload.work_order_closed) {
|
||||||
frm.disable_save();
|
frm.disable_save();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.is_subcontracted) {
|
||||||
|
frm.trigger("make_subcontracting_po");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false;
|
let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false;
|
||||||
|
|
||||||
frm.toggle_enable("for_quantity", !has_stock_entry);
|
frm.toggle_enable("for_quantity", !has_stock_entry);
|
||||||
|
|
||||||
if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
|
if (!frm.is_new() && !frm.doc.skip_material_transfer && has_items && frm.doc.docstatus < 2) {
|
||||||
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
|
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
|
||||||
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||||
|
|
||||||
@@ -63,11 +103,11 @@ frappe.ui.form.on("Job Card", {
|
|||||||
if (to_transfer || excess_transfer_allowed) {
|
if (to_transfer || excess_transfer_allowed) {
|
||||||
frm.add_custom_button(__("Material Transfer"), () => {
|
frm.add_custom_button(__("Material Transfer"), () => {
|
||||||
frm.trigger("make_stock_entry");
|
frm.trigger("make_stock_entry");
|
||||||
}).addClass("btn-primary");
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
|
if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card && !frm.doc.finished_good) {
|
||||||
frm.trigger("setup_corrective_job_card");
|
frm.trigger("setup_corrective_job_card");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,31 +124,67 @@ frappe.ui.form.on("Job Card", {
|
|||||||
frm.trigger("toggle_operation_number");
|
frm.trigger("toggle_operation_number");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
frm.doc.docstatus == 0 &&
|
frm.doc.for_quantity + frm.doc.process_loss_qty > frm.doc.total_completed_qty &&
|
||||||
!frm.is_new() &&
|
(frm.doc.skip_material_transfer ||
|
||||||
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) &&
|
frm.doc.transferred_qty >= frm.doc.for_quantity + frm.doc.process_loss_qty ||
|
||||||
(frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)
|
!frm.doc.finished_good)
|
||||||
) {
|
) {
|
||||||
// if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
|
if (!frm.doc.time_logs?.length) {
|
||||||
// and if stock mvt for WIP is required
|
frm.add_custom_button(__("Start Job"), () => {
|
||||||
if (frm.doc.work_order) {
|
let from_time = frappe.datetime.now_datetime();
|
||||||
frappe.db.get_value(
|
if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
|
||||||
"Work Order",
|
frappe.prompt(
|
||||||
frm.doc.work_order,
|
{
|
||||||
["skip_transfer", "status"],
|
fieldtype: "Table MultiSelect",
|
||||||
(result) => {
|
label: __("Select Employees"),
|
||||||
if (
|
options: "Job Card Time Log",
|
||||||
result.skip_transfer === 1 ||
|
fieldname: "employees",
|
||||||
result.status == "In Process" ||
|
},
|
||||||
frm.doc.transferred_qty > 0 ||
|
(d) => {
|
||||||
!frm.doc.items.length
|
frm.events.start_timer(frm, from_time, d.employees);
|
||||||
) {
|
},
|
||||||
frm.trigger("prepare_timer_buttons");
|
__("Assign Job to Employee")
|
||||||
}
|
);
|
||||||
|
} else {
|
||||||
|
frm.events.start_timer(frm, from_time, frm.doc.employee);
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
} else if (frm.doc.is_paused) {
|
||||||
|
frm.add_custom_button(__("Resume Job"), () => {
|
||||||
|
frm.call({
|
||||||
|
method: "resume_job",
|
||||||
|
doc: frm.doc,
|
||||||
|
args: {
|
||||||
|
start_time: frappe.datetime.now_datetime(),
|
||||||
|
},
|
||||||
|
callback() {
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
frm.trigger("prepare_timer_buttons");
|
if (frm.doc.for_quantity - frm.doc.manufactured_qty > 0) {
|
||||||
|
if (!frm.doc.is_paused) {
|
||||||
|
frm.add_custom_button(__("Pause Job"), () => {
|
||||||
|
frm.call({
|
||||||
|
method: "pause_job",
|
||||||
|
doc: frm.doc,
|
||||||
|
args: {
|
||||||
|
end_time: frappe.datetime.now_datetime(),
|
||||||
|
},
|
||||||
|
callback() {
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.add_custom_button(__("Complete Job"), () => {
|
||||||
|
frm.trigger("complete_job_card");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.trigger("make_dashboard");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +192,7 @@ frappe.ui.form.on("Job Card", {
|
|||||||
|
|
||||||
if (frm.doc.work_order) {
|
if (frm.doc.work_order) {
|
||||||
frappe.db.get_value("Work Order", frm.doc.work_order, "transfer_material_against").then((r) => {
|
frappe.db.get_value("Work Order", frm.doc.work_order, "transfer_material_against").then((r) => {
|
||||||
if (r.message.transfer_material_against == "Work Order") {
|
if (r.message.transfer_material_against == "Work Order" && !frm.doc.operation_row_id) {
|
||||||
frm.set_df_property("items", "hidden", 1);
|
frm.set_df_property("items", "hidden", 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -134,6 +210,75 @@ frappe.ui.form.on("Job Card", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
make_subcontracting_po(frm) {
|
||||||
|
if (frm.doc.docstatus === 1 && frm.doc.for_quantity > frm.doc.manufactured_qty) {
|
||||||
|
frm.add_custom_button(__("Make Subcontracting PO"), () => {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.manufacturing.doctype.job_card.job_card.make_subcontracting_po",
|
||||||
|
frm: frm,
|
||||||
|
});
|
||||||
|
}).addClass("btn-primary");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
start_timer(frm, start_time, employees) {
|
||||||
|
frm.call({
|
||||||
|
method: "start_timer",
|
||||||
|
doc: frm.doc,
|
||||||
|
args: {
|
||||||
|
start_time: start_time,
|
||||||
|
employees: employees,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
frm.reload_doc();
|
||||||
|
frm.trigger("make_dashboard");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
make_finished_good(frm) {
|
||||||
|
let fields = [
|
||||||
|
{
|
||||||
|
fieldtype: "Float",
|
||||||
|
label: __("Completed Quantity"),
|
||||||
|
fieldname: "qty",
|
||||||
|
reqd: 1,
|
||||||
|
default: frm.doc.for_quantity - frm.doc.manufactured_qty,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Datetime",
|
||||||
|
label: __("End Time"),
|
||||||
|
fieldname: "end_time",
|
||||||
|
default: frappe.datetime.now_datetime(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
frappe.prompt(
|
||||||
|
fields,
|
||||||
|
(data) => {
|
||||||
|
if (data.qty <= 0) {
|
||||||
|
frappe.throw(__("Quantity should be greater than 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.call({
|
||||||
|
method: "make_finished_good",
|
||||||
|
doc: frm.doc,
|
||||||
|
args: {
|
||||||
|
qty: data.qty,
|
||||||
|
end_time: data.end_time,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
var doc = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
__("Enter Value"),
|
||||||
|
__("Update"),
|
||||||
|
__("Set Finished Good Quantity")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
setup_quality_inspection: function (frm) {
|
setup_quality_inspection: function (frm) {
|
||||||
let quality_inspection_field = frm.get_docfield("quality_inspection");
|
let quality_inspection_field = frm.get_docfield("quality_inspection");
|
||||||
quality_inspection_field.get_route_options_for_new_doc = function (frm) {
|
quality_inspection_field.get_route_options_for_new_doc = function (frm) {
|
||||||
@@ -262,90 +407,6 @@ frappe.ui.form.on("Job Card", {
|
|||||||
frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
|
frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
|
||||||
},
|
},
|
||||||
|
|
||||||
prepare_timer_buttons: function (frm) {
|
|
||||||
frm.trigger("make_dashboard");
|
|
||||||
|
|
||||||
if (!frm.doc.started_time && !frm.doc.current_time) {
|
|
||||||
frm.add_custom_button(__("Start Job"), () => {
|
|
||||||
if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
|
|
||||||
frappe.prompt(
|
|
||||||
{
|
|
||||||
fieldtype: "Table MultiSelect",
|
|
||||||
label: __("Select Employees"),
|
|
||||||
options: "Job Card Time Log",
|
|
||||||
fieldname: "employees",
|
|
||||||
},
|
|
||||||
(d) => {
|
|
||||||
frm.events.start_job(frm, "Work In Progress", d.employees);
|
|
||||||
},
|
|
||||||
__("Assign Job to Employee")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
|
|
||||||
}
|
|
||||||
}).addClass("btn-primary");
|
|
||||||
} else if (frm.doc.status == "On Hold") {
|
|
||||||
frm.add_custom_button(__("Resume Job"), () => {
|
|
||||||
frm.events.start_job(frm, "Resume Job", frm.doc.employee);
|
|
||||||
}).addClass("btn-primary");
|
|
||||||
} else {
|
|
||||||
frm.add_custom_button(__("Pause Job"), () => {
|
|
||||||
frm.events.complete_job(frm, "On Hold");
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.add_custom_button(__("Complete Job"), () => {
|
|
||||||
var sub_operations = frm.doc.sub_operations;
|
|
||||||
|
|
||||||
let set_qty = true;
|
|
||||||
if (sub_operations && sub_operations.length > 1) {
|
|
||||||
set_qty = false;
|
|
||||||
let last_op_row = sub_operations[sub_operations.length - 2];
|
|
||||||
|
|
||||||
if (last_op_row.status == "Complete") {
|
|
||||||
set_qty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (set_qty) {
|
|
||||||
frappe.prompt(
|
|
||||||
{
|
|
||||||
fieldtype: "Float",
|
|
||||||
label: __("Completed Quantity"),
|
|
||||||
fieldname: "qty",
|
|
||||||
default: frm.doc.for_quantity,
|
|
||||||
},
|
|
||||||
(data) => {
|
|
||||||
frm.events.complete_job(frm, "Complete", data.qty);
|
|
||||||
},
|
|
||||||
__("Enter Value")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
frm.events.complete_job(frm, "Complete", 0.0);
|
|
||||||
}
|
|
||||||
}).addClass("btn-primary");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
start_job: function (frm, status, employee) {
|
|
||||||
const args = {
|
|
||||||
job_card_id: frm.doc.name,
|
|
||||||
start_time: frappe.datetime.now_datetime(),
|
|
||||||
employees: employee,
|
|
||||||
status: status,
|
|
||||||
};
|
|
||||||
frm.events.make_time_log(frm, args);
|
|
||||||
},
|
|
||||||
|
|
||||||
complete_job: function (frm, status, completed_qty) {
|
|
||||||
const args = {
|
|
||||||
job_card_id: frm.doc.name,
|
|
||||||
complete_time: frappe.datetime.now_datetime(),
|
|
||||||
status: status,
|
|
||||||
completed_qty: completed_qty,
|
|
||||||
};
|
|
||||||
frm.events.make_time_log(frm, args);
|
|
||||||
},
|
|
||||||
|
|
||||||
make_time_log: function (frm, args) {
|
make_time_log: function (frm, args) {
|
||||||
frm.events.update_sub_operation(frm, args);
|
frm.events.update_sub_operation(frm, args);
|
||||||
|
|
||||||
@@ -392,7 +453,7 @@ frappe.ui.form.on("Job Card", {
|
|||||||
function updateStopwatch(increment) {
|
function updateStopwatch(increment) {
|
||||||
var hours = Math.floor(increment / 3600);
|
var hours = Math.floor(increment / 3600);
|
||||||
var minutes = Math.floor((increment - hours * 3600) / 60);
|
var minutes = Math.floor((increment - hours * 3600) / 60);
|
||||||
var seconds = increment - hours * 3600 - minutes * 60;
|
var seconds = flt(increment - hours * 3600 - minutes * 60, 2);
|
||||||
|
|
||||||
$(section)
|
$(section)
|
||||||
.find(".hours")
|
.find(".hours")
|
||||||
@@ -415,7 +476,7 @@ frappe.ui.form.on("Job Card", {
|
|||||||
frm.dashboard.refresh();
|
frm.dashboard.refresh();
|
||||||
const timer = `
|
const timer = `
|
||||||
<div class="stopwatch" style="font-weight:bold;margin:0px 13px 0px 2px;
|
<div class="stopwatch" style="font-weight:bold;margin:0px 13px 0px 2px;
|
||||||
color:#545454;font-size:18px;display:inline-block;vertical-align:text-bottom;>
|
color:#545454;font-size:18px;display:inline-block;vertical-align:text-bottom;">
|
||||||
<span class="hours">00</span>
|
<span class="hours">00</span>
|
||||||
<span class="colon">:</span>
|
<span class="colon">:</span>
|
||||||
<span class="minutes">00</span>
|
<span class="minutes">00</span>
|
||||||
@@ -424,21 +485,34 @@ frappe.ui.form.on("Job Card", {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
var section = frm.toolbar.page.add_inner_message(timer);
|
var section = frm.toolbar.page.add_inner_message(timer);
|
||||||
|
let currentIncrement = frm.events.get_current_time(frm);
|
||||||
let currentIncrement = frm.doc.current_time || 0;
|
if (frm.doc.time_logs?.length && frm.doc.time_logs[cint(frm.doc.time_logs.length) - 1].to_time) {
|
||||||
if (frm.doc.started_time || frm.doc.current_time) {
|
updateStopwatch(currentIncrement);
|
||||||
if (frm.doc.status == "On Hold") {
|
} else if (frm.doc.status == "On Hold") {
|
||||||
updateStopwatch(currentIncrement);
|
updateStopwatch(currentIncrement);
|
||||||
} else {
|
} else {
|
||||||
currentIncrement += moment(frappe.datetime.now_datetime()).diff(
|
initialiseTimer();
|
||||||
moment(frm.doc.started_time),
|
|
||||||
"seconds"
|
|
||||||
);
|
|
||||||
initialiseTimer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_current_time(frm) {
|
||||||
|
let current_time = 0;
|
||||||
|
|
||||||
|
frm.doc.time_logs.forEach((d) => {
|
||||||
|
if (d.to_time) {
|
||||||
|
if (d.time_in_mins) {
|
||||||
|
current_time += flt(d.time_in_mins, 2) * 60;
|
||||||
|
} else {
|
||||||
|
current_time += get_seconds_diff(d.to_time, d.from_time);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current_time += get_seconds_diff(frappe.datetime.now_datetime(), d.from_time);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return current_time;
|
||||||
|
},
|
||||||
|
|
||||||
hide_timer: function (frm) {
|
hide_timer: function (frm) {
|
||||||
frm.toolbar.page.inner_toolbar.find(".stopwatch").remove();
|
frm.toolbar.page.inner_toolbar.find(".stopwatch").remove();
|
||||||
},
|
},
|
||||||
@@ -492,6 +566,14 @@ frappe.ui.form.on("Job Card", {
|
|||||||
|
|
||||||
refresh_field("total_completed_qty");
|
refresh_field("total_completed_qty");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
source_warehouse(frm) {
|
||||||
|
if (frm.doc.source_warehouse) {
|
||||||
|
frm.doc.items.forEach((d) => {
|
||||||
|
frappe.model.set_value(d.doctype, d.name, "source_warehouse", frm.doc.source_warehouse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Job Card Time Log", {
|
frappe.ui.form.on("Job Card Time Log", {
|
||||||
@@ -503,3 +585,7 @@ frappe.ui.form.on("Job Card Time Log", {
|
|||||||
frm.set_value("started_time", "");
|
frm.set_value("started_time", "");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function get_seconds_diff(d1, d2) {
|
||||||
|
return moment(d1).diff(d2, "seconds");
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,43 +8,57 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"work_order",
|
"work_order",
|
||||||
"bom_no",
|
|
||||||
"production_item",
|
|
||||||
"employee",
|
"employee",
|
||||||
|
"is_subcontracted",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"company",
|
"company",
|
||||||
"for_quantity",
|
"project",
|
||||||
|
"bom_no",
|
||||||
|
"semi_finished_good__finished_good_section",
|
||||||
|
"finished_good",
|
||||||
|
"production_item",
|
||||||
|
"semi_fg_bom",
|
||||||
"total_completed_qty",
|
"total_completed_qty",
|
||||||
|
"column_break_mcnb",
|
||||||
|
"for_quantity",
|
||||||
|
"transferred_qty",
|
||||||
|
"manufactured_qty",
|
||||||
"process_loss_qty",
|
"process_loss_qty",
|
||||||
|
"production_section",
|
||||||
|
"operation",
|
||||||
|
"source_warehouse",
|
||||||
|
"wip_warehouse",
|
||||||
|
"skip_material_transfer",
|
||||||
|
"backflush_from_wip_warehouse",
|
||||||
|
"column_break_12",
|
||||||
|
"workstation_type",
|
||||||
|
"workstation",
|
||||||
|
"target_warehouse",
|
||||||
|
"section_break_8",
|
||||||
|
"items",
|
||||||
|
"quality_inspection_section",
|
||||||
|
"quality_inspection_template",
|
||||||
|
"column_break_fcmp",
|
||||||
|
"quality_inspection",
|
||||||
|
"scheduled_time_tab",
|
||||||
"scheduled_time_section",
|
"scheduled_time_section",
|
||||||
"expected_start_date",
|
"expected_start_date",
|
||||||
"time_required",
|
"time_required",
|
||||||
"column_break_jkir",
|
"column_break_jkir",
|
||||||
"expected_end_date",
|
"expected_end_date",
|
||||||
"section_break_05am",
|
"section_break_rzeo",
|
||||||
"scheduled_time_logs",
|
"scheduled_time_logs",
|
||||||
"timing_detail",
|
"timing_detail",
|
||||||
"time_logs",
|
|
||||||
"section_break_13",
|
"section_break_13",
|
||||||
"actual_start_date",
|
"actual_start_date",
|
||||||
"total_time_in_mins",
|
"total_time_in_mins",
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
"actual_end_date",
|
"actual_end_date",
|
||||||
"production_section",
|
"section_break_jbas",
|
||||||
"operation",
|
"time_logs",
|
||||||
"wip_warehouse",
|
|
||||||
"column_break_12",
|
|
||||||
"workstation_type",
|
|
||||||
"workstation",
|
|
||||||
"quality_inspection_section",
|
|
||||||
"quality_inspection_template",
|
|
||||||
"column_break_fcmp",
|
|
||||||
"quality_inspection",
|
|
||||||
"section_break_21",
|
"section_break_21",
|
||||||
"sub_operations",
|
"sub_operations",
|
||||||
"section_break_8",
|
|
||||||
"items",
|
|
||||||
"scrap_items_section",
|
"scrap_items_section",
|
||||||
"scrap_items",
|
"scrap_items",
|
||||||
"corrective_operation_section",
|
"corrective_operation_section",
|
||||||
@@ -54,11 +68,11 @@
|
|||||||
"hour_rate",
|
"hour_rate",
|
||||||
"for_operation",
|
"for_operation",
|
||||||
"more_information",
|
"more_information",
|
||||||
"project",
|
|
||||||
"item_name",
|
"item_name",
|
||||||
"transferred_qty",
|
|
||||||
"requested_qty",
|
"requested_qty",
|
||||||
"status",
|
"status",
|
||||||
|
"operation_row_id",
|
||||||
|
"is_paused",
|
||||||
"column_break_20",
|
"column_break_20",
|
||||||
"operation_row_number",
|
"operation_row_number",
|
||||||
"operation_id",
|
"operation_id",
|
||||||
@@ -68,7 +82,6 @@
|
|||||||
"batch_no",
|
"batch_no",
|
||||||
"serial_no",
|
"serial_no",
|
||||||
"barcode",
|
"barcode",
|
||||||
"job_started",
|
|
||||||
"started_time",
|
"started_time",
|
||||||
"current_time",
|
"current_time",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
@@ -86,10 +99,11 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!doc.finished_good",
|
||||||
"fetch_from": "work_order.bom_no",
|
"fetch_from": "work_order.bom_no",
|
||||||
"fieldname": "bom_no",
|
"fieldname": "bom_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "BOM No",
|
"label": "Final BOM",
|
||||||
"options": "BOM",
|
"options": "BOM",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -105,6 +119,7 @@
|
|||||||
"fieldname": "operation",
|
"fieldname": "operation",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
|
"in_preview": 1,
|
||||||
"label": "Operation",
|
"label": "Operation",
|
||||||
"options": "Operation",
|
"options": "Operation",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -130,22 +145,24 @@
|
|||||||
"fieldname": "for_quantity",
|
"fieldname": "for_quantity",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
|
"in_preview": 1,
|
||||||
"label": "Qty To Manufacture"
|
"label": "Qty To Manufacture"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "wip_warehouse",
|
"fieldname": "wip_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "WIP Warehouse",
|
"label": "WIP Warehouse",
|
||||||
"options": "Warehouse",
|
"mandatory_depends_on": "eval:!doc.finished_good || doc.skip_material_transfer === 0 || (doc.skip_material_transfer && doc.backflush_from_wip_warehouse)",
|
||||||
"reqd": 1
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "timing_detail",
|
"fieldname": "timing_detail",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Actual Time"
|
"label": "Actual Time"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 1,
|
"allow_bulk_edit": 1,
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "time_logs",
|
"fieldname": "time_logs",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Time Logs",
|
"label": "Time Logs",
|
||||||
@@ -157,9 +174,12 @@
|
|||||||
"hide_border": 1
|
"hide_border": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.is_subcontracted===0",
|
||||||
"fieldname": "total_completed_qty",
|
"fieldname": "total_completed_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
|
"in_preview": 1,
|
||||||
"label": "Total Completed Qty",
|
"label": "Total Completed Qty",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -175,7 +195,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_8",
|
"fieldname": "section_break_8",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Raw Materials"
|
"label": "Raw Materials"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -199,9 +219,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"depends_on": "items",
|
||||||
"fieldname": "transferred_qty",
|
"fieldname": "transferred_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "FG Qty from Transferred Raw Materials",
|
"label": "Transferred Raw Materials",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -227,6 +248,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"default": "Open",
|
"default": "Open",
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -236,16 +258,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"allow_on_submit": 1,
|
||||||
"fieldname": "job_started",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Job Started",
|
|
||||||
"no_copy": 1,
|
|
||||||
"print_hide": 1,
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "started_time",
|
"fieldname": "started_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
@@ -273,18 +286,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "production_section",
|
"fieldname": "production_section",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Operation & Workstation"
|
"label": "Operation & Materials"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_12",
|
"fieldname": "column_break_12",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!doc.finished_good",
|
||||||
"fetch_from": "work_order.production_item",
|
"fetch_from": "work_order.production_item",
|
||||||
"fieldname": "production_item",
|
"fieldname": "production_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Production Item",
|
"label": "Final Product",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -302,6 +316,7 @@
|
|||||||
"label": "Item Name"
|
"label": "Item Name"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "current_time",
|
"fieldname": "current_time",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
@@ -384,6 +399,7 @@
|
|||||||
"options": "Operation"
|
"options": "Operation"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "employee",
|
"fieldname": "employee",
|
||||||
"fieldtype": "Table MultiSelect",
|
"fieldtype": "Table MultiSelect",
|
||||||
"label": "Employee",
|
"label": "Employee",
|
||||||
@@ -463,6 +479,7 @@
|
|||||||
"show_dashboard": 1
|
"show_dashboard": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "expected_start_date",
|
||||||
"fieldname": "scheduled_time_section",
|
"fieldname": "scheduled_time_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Scheduled Time"
|
"label": "Scheduled Time"
|
||||||
@@ -476,10 +493,6 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Expected Time Required (In Mins)"
|
"label": "Expected Time Required (In Mins)"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_05am",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "scheduled_time_logs",
|
"fieldname": "scheduled_time_logs",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
@@ -507,11 +520,101 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_fcmp",
|
"fieldname": "column_break_fcmp",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "finished_good",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_preview": 1,
|
||||||
|
"label": "Finished Good",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "target_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Target Warehouse",
|
||||||
|
"mandatory_depends_on": "eval:doc.finished_good",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "operation_row_id",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Operation Row ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "source_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Source Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "semi_finished_good__finished_good_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Semi Finished Good / Finished Good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "semi_fg_bom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Semi Finished Goods BOM",
|
||||||
|
"options": "BOM",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_subcontracted",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": " Is Subcontracted",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_mcnb",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_rzeo",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_jbas",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "scheduled_time_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Scheduled Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "finished_good",
|
||||||
|
"fieldname": "manufactured_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Manufactured Qty",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.finished_good",
|
||||||
|
"fieldname": "skip_material_transfer",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Skip Material Transfer to WIP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.finished_good && doc.skip_material_transfer === 1",
|
||||||
|
"fieldname": "backflush_from_wip_warehouse",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Backflush Materials From WIP Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_paused",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Paused",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:09:56.634418",
|
"modified": "2024-06-03 17:44:18.324743",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
@@ -564,6 +667,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"show_preview_popup": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from frappe import _, bold
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder import Criterion
|
from frappe.query_builder import Criterion
|
||||||
from frappe.query_builder.functions import IfNull, Max, Min
|
from frappe.query_builder.functions import IfNull, Max, Min, Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
add_to_date,
|
add_to_date,
|
||||||
@@ -21,13 +21,15 @@ from frappe.utils import (
|
|||||||
getdate,
|
getdate,
|
||||||
time_diff,
|
time_diff,
|
||||||
time_diff_in_hours,
|
time_diff_in_hours,
|
||||||
time_diff_in_seconds,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
|
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
|
||||||
get_mins_between_operations,
|
get_mins_between_operations,
|
||||||
)
|
)
|
||||||
from erpnext.manufacturing.doctype.workstation_type.workstation_type import get_workstations
|
from erpnext.manufacturing.doctype.workstation_type.workstation_type import get_workstations
|
||||||
|
from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
|
||||||
|
get_subcontracting_boms_for_finished_goods,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OverlapError(frappe.ValidationError):
|
class OverlapError(frappe.ValidationError):
|
||||||
@@ -64,14 +66,13 @@ class JobCard(Document):
|
|||||||
from erpnext.manufacturing.doctype.job_card_scheduled_time.job_card_scheduled_time import (
|
from erpnext.manufacturing.doctype.job_card_scheduled_time.job_card_scheduled_time import (
|
||||||
JobCardScheduledTime,
|
JobCardScheduledTime,
|
||||||
)
|
)
|
||||||
from erpnext.manufacturing.doctype.job_card_scrap_item.job_card_scrap_item import (
|
from erpnext.manufacturing.doctype.job_card_scrap_item.job_card_scrap_item import JobCardScrapItem
|
||||||
JobCardScrapItem,
|
|
||||||
)
|
|
||||||
from erpnext.manufacturing.doctype.job_card_time_log.job_card_time_log import JobCardTimeLog
|
from erpnext.manufacturing.doctype.job_card_time_log.job_card_time_log import JobCardTimeLog
|
||||||
|
|
||||||
actual_end_date: DF.Datetime | None
|
actual_end_date: DF.Datetime | None
|
||||||
actual_start_date: DF.Datetime | None
|
actual_start_date: DF.Datetime | None
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
|
backflush_from_wip_warehouse: DF.Check
|
||||||
barcode: DF.Barcode | None
|
barcode: DF.Barcode | None
|
||||||
batch_no: DF.Link | None
|
batch_no: DF.Link | None
|
||||||
bom_no: DF.Link | None
|
bom_no: DF.Link | None
|
||||||
@@ -80,18 +81,22 @@ class JobCard(Document):
|
|||||||
employee: DF.TableMultiSelect[JobCardTimeLog]
|
employee: DF.TableMultiSelect[JobCardTimeLog]
|
||||||
expected_end_date: DF.Datetime | None
|
expected_end_date: DF.Datetime | None
|
||||||
expected_start_date: DF.Datetime | None
|
expected_start_date: DF.Datetime | None
|
||||||
|
finished_good: DF.Link | None
|
||||||
for_job_card: DF.Link | None
|
for_job_card: DF.Link | None
|
||||||
for_operation: DF.Link | None
|
for_operation: DF.Link | None
|
||||||
for_quantity: DF.Float
|
for_quantity: DF.Float
|
||||||
hour_rate: DF.Currency
|
hour_rate: DF.Currency
|
||||||
is_corrective_job_card: DF.Check
|
is_corrective_job_card: DF.Check
|
||||||
|
is_paused: DF.Check
|
||||||
|
is_subcontracted: DF.Check
|
||||||
item_name: DF.ReadOnly | None
|
item_name: DF.ReadOnly | None
|
||||||
items: DF.Table[JobCardItem]
|
items: DF.Table[JobCardItem]
|
||||||
job_started: DF.Check
|
manufactured_qty: DF.Float
|
||||||
naming_series: DF.Literal["PO-JOB.#####"]
|
naming_series: DF.Literal["PO-JOB.#####"]
|
||||||
operation: DF.Link
|
operation: DF.Link
|
||||||
operation_id: DF.Data | None
|
operation_id: DF.Data | None
|
||||||
operation_row_number: DF.Literal
|
operation_row_id: DF.Int
|
||||||
|
operation_row_number: DF.Literal[None]
|
||||||
posting_date: DF.Date | None
|
posting_date: DF.Date | None
|
||||||
process_loss_qty: DF.Float
|
process_loss_qty: DF.Float
|
||||||
production_item: DF.Link | None
|
production_item: DF.Link | None
|
||||||
@@ -102,9 +107,12 @@ class JobCard(Document):
|
|||||||
requested_qty: DF.Float
|
requested_qty: DF.Float
|
||||||
scheduled_time_logs: DF.Table[JobCardScheduledTime]
|
scheduled_time_logs: DF.Table[JobCardScheduledTime]
|
||||||
scrap_items: DF.Table[JobCardScrapItem]
|
scrap_items: DF.Table[JobCardScrapItem]
|
||||||
|
semi_fg_bom: DF.Link | None
|
||||||
sequence_id: DF.Int
|
sequence_id: DF.Int
|
||||||
serial_and_batch_bundle: DF.Link | None
|
serial_and_batch_bundle: DF.Link | None
|
||||||
serial_no: DF.SmallText | None
|
serial_no: DF.SmallText | None
|
||||||
|
skip_material_transfer: DF.Check
|
||||||
|
source_warehouse: DF.Link | None
|
||||||
started_time: DF.Datetime | None
|
started_time: DF.Datetime | None
|
||||||
status: DF.Literal[
|
status: DF.Literal[
|
||||||
"Open",
|
"Open",
|
||||||
@@ -116,12 +124,13 @@ class JobCard(Document):
|
|||||||
"Completed",
|
"Completed",
|
||||||
]
|
]
|
||||||
sub_operations: DF.Table[JobCardOperation]
|
sub_operations: DF.Table[JobCardOperation]
|
||||||
|
target_warehouse: DF.Link | None
|
||||||
time_logs: DF.Table[JobCardTimeLog]
|
time_logs: DF.Table[JobCardTimeLog]
|
||||||
time_required: DF.Float
|
time_required: DF.Float
|
||||||
total_completed_qty: DF.Float
|
total_completed_qty: DF.Float
|
||||||
total_time_in_mins: DF.Float
|
total_time_in_mins: DF.Float
|
||||||
transferred_qty: DF.Float
|
transferred_qty: DF.Float
|
||||||
wip_warehouse: DF.Link
|
wip_warehouse: DF.Link | None
|
||||||
work_order: DF.Link
|
work_order: DF.Link
|
||||||
workstation: DF.Link
|
workstation: DF.Link
|
||||||
workstation_type: DF.Link | None
|
workstation_type: DF.Link | None
|
||||||
@@ -141,6 +150,7 @@ class JobCard(Document):
|
|||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_time_logs()
|
self.validate_time_logs()
|
||||||
|
self.validate_on_hold()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.validate_operation_id()
|
self.validate_operation_id()
|
||||||
self.validate_sequence_id()
|
self.validate_sequence_id()
|
||||||
@@ -151,6 +161,31 @@ class JobCard(Document):
|
|||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.validate_job_card_qty()
|
self.validate_job_card_qty()
|
||||||
|
|
||||||
|
def validate_on_hold(self):
|
||||||
|
if self.is_paused and not self.time_logs:
|
||||||
|
self.is_paused = 0
|
||||||
|
|
||||||
|
def set_manufactured_qty(self):
|
||||||
|
table_name = "Stock Entry"
|
||||||
|
if self.is_subcontracted:
|
||||||
|
table_name = "Subcontracting Receipt Item"
|
||||||
|
|
||||||
|
table = frappe.qb.DocType(table_name)
|
||||||
|
query = frappe.qb.from_(table).where((table.job_card == self.name) & (table.docstatus == 1))
|
||||||
|
|
||||||
|
if self.is_subcontracted:
|
||||||
|
query = query.select(Sum(table.qty))
|
||||||
|
else:
|
||||||
|
query = query.select(Sum(table.fg_completed_qty))
|
||||||
|
query = query.where(table.purpose == "Manufacture")
|
||||||
|
|
||||||
|
qty = query.run()[0][0] or 0.0
|
||||||
|
self.manufactured_qty = flt(qty)
|
||||||
|
self.db_set("manufactured_qty", self.manufactured_qty)
|
||||||
|
|
||||||
|
self.update_semi_finished_good_details()
|
||||||
|
self.set_status(update_status=True)
|
||||||
|
|
||||||
def validate_job_card_qty(self):
|
def validate_job_card_qty(self):
|
||||||
if not (self.operation_id and self.work_order):
|
if not (self.operation_id and self.work_order):
|
||||||
return
|
return
|
||||||
@@ -511,13 +546,14 @@ class JobCard(Document):
|
|||||||
if self.time_logs and len(self.time_logs) > 0:
|
if self.time_logs and len(self.time_logs) > 0:
|
||||||
last_row = self.time_logs[-1]
|
last_row = self.time_logs[-1]
|
||||||
|
|
||||||
self.reset_timer_value(args)
|
|
||||||
if last_row and args.get("complete_time"):
|
if last_row and args.get("complete_time"):
|
||||||
for row in self.time_logs:
|
for row in self.time_logs:
|
||||||
if not row.to_time:
|
if not row.to_time:
|
||||||
row.update(
|
to_time = get_datetime(args.get("complete_time"))
|
||||||
|
row.db_set(
|
||||||
{
|
{
|
||||||
"to_time": get_datetime(args.get("complete_time")),
|
"to_time": to_time,
|
||||||
|
"time_in_mins": time_diff_in_minutes(to_time, row.from_time),
|
||||||
"operation": args.get("sub_operation"),
|
"operation": args.get("sub_operation"),
|
||||||
"completed_qty": (args.get("completed_qty") if last_row.idx == row.idx else 0.0),
|
"completed_qty": (args.get("completed_qty") if last_row.idx == row.idx else 0.0),
|
||||||
}
|
}
|
||||||
@@ -538,35 +574,17 @@ class JobCard(Document):
|
|||||||
else:
|
else:
|
||||||
self.add_start_time_log(new_args)
|
self.add_start_time_log(new_args)
|
||||||
|
|
||||||
if not self.employee and employees:
|
|
||||||
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 add_start_time_log(self, args):
|
def add_start_time_log(self, args):
|
||||||
self.append("time_logs", args)
|
if args.from_time and args.to_time:
|
||||||
|
args.time_in_mins = time_diff_in_minutes(args.to_time, args.from_time)
|
||||||
|
|
||||||
|
row = self.append("time_logs", args)
|
||||||
|
row.db_update()
|
||||||
|
|
||||||
def set_employees(self, employees):
|
def set_employees(self, employees):
|
||||||
for name in employees:
|
for name in employees:
|
||||||
self.append("employee", {"employee": name.get("employee"), "completed_qty": 0.0})
|
self.append("employee", {"employee": name.get("employee"), "completed_qty": 0.0})
|
||||||
|
self.save()
|
||||||
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):
|
def update_sub_operation_status(self):
|
||||||
if not (self.sub_operations and self.time_logs):
|
if not (self.sub_operations and self.time_logs):
|
||||||
@@ -628,23 +646,25 @@ class JobCard(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
doc = frappe.get_doc("Work Order", self.get("work_order"))
|
doc = frappe.get_doc("Work Order", self.get("work_order"))
|
||||||
if doc.transfer_material_against == "Work Order" or doc.skip_transfer:
|
if not doc.track_semi_finished_goods and (
|
||||||
|
doc.transfer_material_against == "Work Order" or doc.skip_transfer
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in doc.required_items:
|
for d in doc.required_items:
|
||||||
if not d.operation:
|
if not d.operation and not d.operation_row_id:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row {0} : Operation is required against the raw material item {1}").format(
|
_("Row {0} : Operation is required against the raw material item {1}").format(
|
||||||
d.idx, d.item_code
|
d.idx, d.item_code
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.get("operation") == d.operation:
|
if self.get("operation") == d.operation or self.operation_row_id == d.operation_row_id:
|
||||||
self.append(
|
self.append(
|
||||||
"items",
|
"items",
|
||||||
{
|
{
|
||||||
"item_code": d.item_code,
|
"item_code": d.item_code,
|
||||||
"source_warehouse": d.source_warehouse,
|
"source_warehouse": self.source_warehouse or 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,
|
||||||
@@ -669,7 +689,7 @@ class JobCard(Document):
|
|||||||
self.set_transferred_qty()
|
self.set_transferred_qty()
|
||||||
|
|
||||||
def validate_transfer_qty(self):
|
def validate_transfer_qty(self):
|
||||||
if self.items and self.transferred_qty < self.for_quantity:
|
if not self.finished_good and self.items and self.transferred_qty < self.for_quantity:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
|
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
|
||||||
@@ -677,6 +697,9 @@ class JobCard(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_job_card(self):
|
def validate_job_card(self):
|
||||||
|
if self.finished_good:
|
||||||
|
return
|
||||||
|
|
||||||
if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped":
|
if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped":
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Transaction not allowed against stopped Work Order {0}").format(
|
_("Transaction not allowed against stopped Work Order {0}").format(
|
||||||
@@ -745,6 +768,9 @@ class JobCard(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_work_order(self):
|
def update_work_order(self):
|
||||||
|
if self.finished_good:
|
||||||
|
return
|
||||||
|
|
||||||
if not self.work_order:
|
if not self.work_order:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -756,7 +782,6 @@ class JobCard(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for_quantity, time_in_mins, process_loss_qty = 0, 0, 0
|
for_quantity, time_in_mins, process_loss_qty = 0, 0, 0
|
||||||
_from_time_list, _to_time_list = [], []
|
|
||||||
|
|
||||||
data = self.get_current_operation_data()
|
data = self.get_current_operation_data()
|
||||||
if data and len(data) > 0:
|
if data and len(data) > 0:
|
||||||
@@ -773,6 +798,20 @@ class JobCard(Document):
|
|||||||
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
|
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
|
||||||
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
|
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
|
||||||
|
|
||||||
|
def update_semi_finished_good_details(self):
|
||||||
|
if self.operation_id:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Work Order Operation", self.operation_id, "completed_qty", self.manufactured_qty
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.finished_good
|
||||||
|
and frappe.get_cached_value("Work Order", self.work_order, "production_item")
|
||||||
|
== self.finished_good
|
||||||
|
):
|
||||||
|
_wo_doc = frappe.get_doc("Work Order", self.work_order)
|
||||||
|
_wo_doc.db_set("produced_qty", self.manufactured_qty)
|
||||||
|
_wo_doc.db_set("status", _wo_doc.get_status())
|
||||||
|
|
||||||
def update_corrective_in_work_order(self, wo):
|
def update_corrective_in_work_order(self, wo):
|
||||||
wo.corrective_operation_cost = 0.0
|
wo.corrective_operation_cost = 0.0
|
||||||
for row in frappe.get_all(
|
for row in frappe.get_all(
|
||||||
@@ -913,64 +952,71 @@ class JobCard(Document):
|
|||||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
|
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
|
||||||
|
|
||||||
def set_transferred_qty(self, update_status=False):
|
def set_transferred_qty(self, update_status=False):
|
||||||
"Set total FG Qty in Job Card for which RM was transferred."
|
from frappe.query_builder.functions import Sum
|
||||||
if not self.items:
|
|
||||||
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
|
||||||
|
|
||||||
doc = frappe.get_doc("Work Order", self.get("work_order"))
|
stock_entry = frappe.qb.DocType("Stock Entry")
|
||||||
if doc.transfer_material_against == "Work Order" or doc.skip_transfer:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.items:
|
query = (
|
||||||
# sum of 'For Quantity' of Stock Entries against JC
|
frappe.qb.from_(stock_entry)
|
||||||
self.transferred_qty = (
|
.select(Sum(stock_entry.fg_completed_qty))
|
||||||
frappe.db.get_value(
|
.where(
|
||||||
"Stock Entry",
|
(stock_entry.job_card == self.name)
|
||||||
{
|
& (stock_entry.docstatus == 1)
|
||||||
"job_card": self.name,
|
& (stock_entry.purpose == "Material Transfer for Manufacture")
|
||||||
"work_order": self.work_order,
|
|
||||||
"docstatus": 1,
|
|
||||||
"purpose": "Material Transfer for Manufacture",
|
|
||||||
},
|
|
||||||
"sum(fg_completed_qty)",
|
|
||||||
)
|
|
||||||
or 0
|
|
||||||
)
|
)
|
||||||
|
.groupby(stock_entry.job_card)
|
||||||
|
)
|
||||||
|
|
||||||
self.db_set("transferred_qty", self.transferred_qty)
|
query = query.run()
|
||||||
|
|
||||||
qty = 0
|
qty = 0
|
||||||
if self.work_order:
|
|
||||||
doc = frappe.get_doc("Work Order", self.work_order)
|
|
||||||
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
|
|
||||||
completed = True
|
|
||||||
for d in doc.operations:
|
|
||||||
if d.status != "Completed":
|
|
||||||
completed = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if completed:
|
if query and query[0][0]:
|
||||||
job_cards = frappe.get_all(
|
qty = flt(query[0][0])
|
||||||
"Job Card",
|
|
||||||
filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
|
|
||||||
fields="sum(transferred_qty) as qty",
|
|
||||||
group_by="operation_id",
|
|
||||||
)
|
|
||||||
|
|
||||||
if job_cards:
|
self.db_set("transferred_qty", qty)
|
||||||
qty = min(d.qty for d in job_cards)
|
self.set_status(update_status)
|
||||||
|
|
||||||
|
if self.work_order and not frappe.get_cached_value(
|
||||||
|
"Work Order", self.work_order, "track_semi_finished_goods"
|
||||||
|
):
|
||||||
|
self.set_transferred_qty_in_work_order()
|
||||||
|
|
||||||
|
def set_transferred_qty_in_work_order(self):
|
||||||
|
doc = frappe.get_doc("Work Order", self.work_order)
|
||||||
|
|
||||||
|
qty = 0.0
|
||||||
|
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
|
||||||
|
completed = True
|
||||||
|
for d in doc.operations:
|
||||||
|
if d.status != "Completed":
|
||||||
|
completed = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if completed:
|
||||||
|
job_cards = frappe.get_all(
|
||||||
|
"Job Card",
|
||||||
|
filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
|
||||||
|
fields="sum(transferred_qty) as qty",
|
||||||
|
group_by="operation_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
if job_cards:
|
||||||
|
qty = min(d.qty for d in job_cards)
|
||||||
|
|
||||||
doc.db_set("material_transferred_for_manufacturing", qty)
|
doc.db_set("material_transferred_for_manufacturing", qty)
|
||||||
|
|
||||||
self.set_status(update_status)
|
|
||||||
|
|
||||||
def set_status(self, update_status=False):
|
def set_status(self, update_status=False):
|
||||||
if self.status == "On Hold" and self.docstatus == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||||
|
if self.finished_good and self.docstatus == 1:
|
||||||
|
if self.manufactured_qty >= self.for_quantity:
|
||||||
|
self.status = "Completed"
|
||||||
|
elif self.transferred_qty > 0 or self.skip_material_transfer:
|
||||||
|
self.status = "Work In Progress"
|
||||||
|
|
||||||
if self.docstatus < 2:
|
if self.docstatus == 0 and self.time_logs:
|
||||||
|
self.status = "Work In Progress"
|
||||||
|
|
||||||
|
if not self.finished_good and self.docstatus < 2:
|
||||||
if flt(self.for_quantity) <= flt(self.transferred_qty):
|
if flt(self.for_quantity) <= flt(self.transferred_qty):
|
||||||
self.status = "Material Transferred"
|
self.status = "Material Transferred"
|
||||||
|
|
||||||
@@ -980,16 +1026,14 @@ class JobCard(Document):
|
|||||||
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
|
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
|
||||||
self.status = "Completed"
|
self.status = "Completed"
|
||||||
|
|
||||||
|
if self.is_paused:
|
||||||
|
self.status = "On Hold"
|
||||||
|
|
||||||
if update_status:
|
if update_status:
|
||||||
self.db_set("status", self.status)
|
self.db_set("status", self.status)
|
||||||
|
|
||||||
if self.status in ["Completed", "Work In Progress"]:
|
if self.workstation:
|
||||||
status = {
|
self.update_workstation_status()
|
||||||
"Completed": "Off",
|
|
||||||
"Work In Progress": "Production",
|
|
||||||
}.get(self.status)
|
|
||||||
|
|
||||||
self.update_status_in_workstation(status)
|
|
||||||
|
|
||||||
def set_wip_warehouse(self):
|
def set_wip_warehouse(self):
|
||||||
if not self.wip_warehouse:
|
if not self.wip_warehouse:
|
||||||
@@ -1012,7 +1056,30 @@ class JobCard(Document):
|
|||||||
OperationMismatchError,
|
OperationMismatchError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def pause_job(self, **kwargs):
|
||||||
|
if isinstance(kwargs, dict):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
self.db_set("is_paused", 1)
|
||||||
|
self.add_time_logs(to_time=kwargs.end_time, completed_qty=0.0, employees=self.employee)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def resume_job(self, **kwargs):
|
||||||
|
if isinstance(kwargs, dict):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
self.db_set("is_paused", 0)
|
||||||
|
self.add_time_logs(
|
||||||
|
from_time=kwargs.start_time,
|
||||||
|
employees=self.employee,
|
||||||
|
completed_qty=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
def validate_sequence_id(self):
|
def validate_sequence_id(self):
|
||||||
|
if self.is_new():
|
||||||
|
return
|
||||||
|
|
||||||
if self.is_corrective_job_card:
|
if self.is_corrective_job_card:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1038,6 +1105,14 @@ class JobCard(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for row in data:
|
for row in data:
|
||||||
|
if not row.completed_qty:
|
||||||
|
frappe.throw(
|
||||||
|
_("{0}, complete the operation {1} before the operation {2}.").format(
|
||||||
|
message, bold(row.operation), bold(self.operation)
|
||||||
|
),
|
||||||
|
OperationSequenceError,
|
||||||
|
)
|
||||||
|
|
||||||
if row.status != "Completed" and row.completed_qty < current_operation_qty:
|
if row.status != "Completed" and row.completed_qty < current_operation_qty:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("{0}, complete the operation {1} before the operation {2}.").format(
|
_("{0}, complete the operation {1} before the operation {2}.").format(
|
||||||
@@ -1075,16 +1150,173 @@ class JobCard(Document):
|
|||||||
|
|
||||||
frappe.db.set_value("Workstation", self.workstation, "status", status)
|
frappe.db.set_value("Workstation", self.workstation, "status", status)
|
||||||
|
|
||||||
|
def add_time_logs(self, **kwargs):
|
||||||
|
row = None
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
update_status = False
|
||||||
|
for employee in kwargs.employees:
|
||||||
|
kwargs.employee = employee.get("employee")
|
||||||
|
if kwargs.from_time and not kwargs.to_time:
|
||||||
|
row = self.append("time_logs", kwargs)
|
||||||
|
row.db_update()
|
||||||
|
self.db_set("status", "Work In Progress")
|
||||||
|
else:
|
||||||
|
update_status = True
|
||||||
|
for row in self.time_logs:
|
||||||
|
if row.to_time or row.employee != kwargs.employee:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row.to_time = kwargs.to_time
|
||||||
|
row.time_in_mins = time_diff_in_minutes(row.to_time, row.from_time)
|
||||||
|
|
||||||
|
if kwargs.employees[-1].get("employee") == row.employee:
|
||||||
|
row.completed_qty = kwargs.completed_qty
|
||||||
|
|
||||||
|
row.db_update()
|
||||||
|
|
||||||
|
self.set_status(update_status=update_status)
|
||||||
|
|
||||||
|
if not self.employee and kwargs.employees:
|
||||||
|
self.set_employees(kwargs.employees)
|
||||||
|
|
||||||
|
def update_workstation_status(self):
|
||||||
|
status_map = {
|
||||||
|
"Open": "Off",
|
||||||
|
"Work In Progress": "Production",
|
||||||
|
"Completed": "Off",
|
||||||
|
"On Hold": "Idle",
|
||||||
|
}
|
||||||
|
|
||||||
|
job_cards = frappe.get_all(
|
||||||
|
"Job Card",
|
||||||
|
fields=["name", "status"],
|
||||||
|
filters={"workstation": self.workstation, "docstatus": 0, "status": ("!=", "Completed")},
|
||||||
|
order_by="status desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not job_cards:
|
||||||
|
frappe.db.set_value("Workstation", self.workstation, "status", "Off")
|
||||||
|
|
||||||
|
for row in job_cards:
|
||||||
|
frappe.db.set_value("Workstation", self.workstation, "status", status_map.get(row.status))
|
||||||
|
return
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def start_timer(self, **kwargs):
|
||||||
|
if isinstance(kwargs, dict):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
if isinstance(kwargs.employees, str):
|
||||||
|
kwargs.employees = [{"employee": kwargs.employees}]
|
||||||
|
|
||||||
|
if kwargs.start_time:
|
||||||
|
self.add_time_logs(from_time=kwargs.start_time, employees=kwargs.employees)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def complete_job_card(self, **kwargs):
|
||||||
|
if isinstance(kwargs, dict):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
if kwargs.end_time:
|
||||||
|
self.add_time_logs(to_time=kwargs.end_time, completed_qty=kwargs.qty, employees=self.employee)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
if kwargs.auto_submit:
|
||||||
|
self.submit()
|
||||||
|
self.make_stock_entry_for_semi_fg_item(kwargs.auto_submit)
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Job Card {0} has been completed").format(get_link_to_form("Job Card", self.name))
|
||||||
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_stock_entry_for_semi_fg_item(self, auto_submit=False):
|
||||||
|
from erpnext.stock.doctype.stock_entry_type.stock_entry_type import ManufactureEntry
|
||||||
|
|
||||||
|
ste = ManufactureEntry(
|
||||||
|
{
|
||||||
|
"for_quantity": self.for_quantity - self.manufactured_qty,
|
||||||
|
"job_card": self.name,
|
||||||
|
"skip_material_transfer": self.skip_material_transfer,
|
||||||
|
"backflush_from_wip_warehouse": self.backflush_from_wip_warehouse,
|
||||||
|
"work_order": self.work_order,
|
||||||
|
"purpose": "Manufacture",
|
||||||
|
"production_item": self.finished_good,
|
||||||
|
"company": self.company,
|
||||||
|
"wip_warehouse": self.wip_warehouse,
|
||||||
|
"fg_warehouse": self.target_warehouse,
|
||||||
|
"bom_no": self.semi_fg_bom,
|
||||||
|
"project": frappe.db.get_value("Work Order", self.work_order, "project"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ste.make_stock_entry()
|
||||||
|
ste.stock_entry.flags.ignore_mandatory = True
|
||||||
|
ste.stock_entry.save()
|
||||||
|
|
||||||
|
if auto_submit:
|
||||||
|
ste.stock_entry.submit()
|
||||||
|
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Stock Entry {0} has created").format(get_link_to_form("Stock Entry", ste.stock_entry.name))
|
||||||
|
)
|
||||||
|
|
||||||
|
return ste.stock_entry.as_dict()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_time_log(args):
|
def make_subcontracting_po(source_name, target_doc=None):
|
||||||
if isinstance(args, str):
|
def set_missing_values(source, target):
|
||||||
args = json.loads(args)
|
_item_details = get_subcontracting_boms_for_finished_goods(source.finished_good)
|
||||||
|
|
||||||
args = frappe._dict(args)
|
pending_qty = source.for_quantity - source.manufactured_qty
|
||||||
doc = frappe.get_doc("Job Card", args.job_card_id)
|
service_item_qty = flt(_item_details.service_item_qty) or 1.0
|
||||||
|
fg_item_qty = flt(_item_details.finished_good_qty) or 1.0
|
||||||
|
|
||||||
|
target.is_subcontracted = 1
|
||||||
|
target.supplier_warehouse = source.wip_warehouse
|
||||||
|
target.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": _item_details.service_item,
|
||||||
|
"fg_item": source.finished_good,
|
||||||
|
"uom": _item_details.service_item_uom,
|
||||||
|
"stock_uom": _item_details.service_item_uom,
|
||||||
|
"conversion_factor": _item_details.conversion_factor or 1,
|
||||||
|
"item_name": _item_details.service_item,
|
||||||
|
"qty": pending_qty * service_item_qty / fg_item_qty,
|
||||||
|
"fg_item_qty": pending_qty,
|
||||||
|
"job_card": source.name,
|
||||||
|
"bom": source.semi_fg_bom,
|
||||||
|
"warehouse": source.target_warehouse,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
doclist = get_mapped_doc(
|
||||||
|
"Job Card",
|
||||||
|
source_name,
|
||||||
|
{
|
||||||
|
"Job Card": {
|
||||||
|
"doctype": "Purchase Order",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
target_doc,
|
||||||
|
set_missing_values,
|
||||||
|
)
|
||||||
|
|
||||||
|
return doclist
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_time_log(kwargs):
|
||||||
|
if isinstance(kwargs, str):
|
||||||
|
kwargs = json.loads(kwargs)
|
||||||
|
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
doc = frappe.get_doc("Job Card", kwargs.job_card_id)
|
||||||
doc.validate_sequence_id()
|
doc.validate_sequence_id()
|
||||||
doc.add_time_log(args)
|
doc.add_time_log(kwargs)
|
||||||
|
doc.set_status(update_status=True)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -1271,7 +1503,6 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
|
|||||||
target.set("sub_operations", [])
|
target.set("sub_operations", [])
|
||||||
target.set_sub_operations()
|
target.set_sub_operations()
|
||||||
target.get_required_items()
|
target.get_required_items()
|
||||||
target.validate_time_logs()
|
|
||||||
|
|
||||||
doclist = get_mapped_doc(
|
doclist = get_mapped_doc(
|
||||||
"Job Card",
|
"Job Card",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ def get_data():
|
|||||||
"non_standard_fieldnames": {"Quality Inspection": "reference_name"},
|
"non_standard_fieldnames": {"Quality Inspection": "reference_name"},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{"label": _("Transactions"), "items": ["Material Request", "Stock Entry"]},
|
{"label": _("Transactions"), "items": ["Material Request", "Stock Entry"]},
|
||||||
|
{"label": _("Subcontracting"), "items": ["Purchase Order", "Subcontracting Order"]},
|
||||||
{"label": _("Reference"), "items": ["Quality Inspection"]},
|
{"label": _("Reference"), "items": ["Quality Inspection"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,14 @@
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "from_time",
|
"fieldname": "from_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "From Time"
|
"label": "From Time"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "to_time",
|
"fieldname": "to_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "time_in_mins",
|
"fieldname": "time_in_mins",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "completed_qty",
|
"fieldname": "completed_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
@@ -63,7 +67,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-05-21 12:40:55.765860",
|
"modified": "2024-05-21 12:41:55.765860",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card Time Log",
|
"name": "Job Card Time Log",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
|
"in_preview": 1,
|
||||||
"label": "Description"
|
"label": "Description"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,7 +105,7 @@
|
|||||||
"icon": "fa fa-wrench",
|
"icon": "fa fa-wrench",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:06.841479",
|
"modified": "2024-05-26 17:59:44.338741",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Operation",
|
"name": "Operation",
|
||||||
@@ -134,6 +135,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"show_preview_popup": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -400,13 +400,19 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
download_materials_required(frm) {
|
download_materials_required(frm) {
|
||||||
|
const warehouses_data = [
|
||||||
|
{
|
||||||
|
warehouse: frm.doc.for_warehouse,
|
||||||
|
},
|
||||||
|
];
|
||||||
const fields = [
|
const fields = [
|
||||||
{
|
{
|
||||||
fieldname: "warehouses",
|
fieldname: "warehouses",
|
||||||
fieldtype: "Table MultiSelect",
|
fieldtype: "Table MultiSelect",
|
||||||
label: __("Warehouses"),
|
label: __("Warehouses"),
|
||||||
default: frm.doc.from_warehouse,
|
default: warehouses_data,
|
||||||
options: "Production Plan Material Request Warehouse",
|
options: "Production Plan Material Request Warehouse",
|
||||||
|
reqd: 1,
|
||||||
get_query: function () {
|
get_query: function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
@@ -452,6 +452,10 @@ class ProductionPlan(Document):
|
|||||||
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bom_no = data.bom_no or item_details and item_details.bom_no or ""
|
||||||
|
if not bom_no:
|
||||||
|
continue
|
||||||
|
|
||||||
pi = self.append(
|
pi = self.append(
|
||||||
"po_items",
|
"po_items",
|
||||||
{
|
{
|
||||||
@@ -459,7 +463,7 @@ class ProductionPlan(Document):
|
|||||||
"item_code": data.item_code,
|
"item_code": data.item_code,
|
||||||
"description": data.description or item_details.description,
|
"description": data.description or item_details.description,
|
||||||
"stock_uom": item_details and item_details.stock_uom or "",
|
"stock_uom": item_details and item_details.stock_uom or "",
|
||||||
"bom_no": data.bom_no or item_details and item_details.bom_no or "",
|
"bom_no": bom_no,
|
||||||
"planned_qty": data.pending_qty,
|
"planned_qty": data.pending_qty,
|
||||||
"pending_qty": data.pending_qty,
|
"pending_qty": data.pending_qty,
|
||||||
"planned_start_date": now_datetime(),
|
"planned_start_date": now_datetime(),
|
||||||
|
|||||||
@@ -328,6 +328,28 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
|
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
|
||||||
|
|
||||||
|
def test_production_plan_with_non_active_bom_item(self):
|
||||||
|
item = make_item("Test Production Item 1 for Non Active BOM", {"is_stock_item": 1}).name
|
||||||
|
|
||||||
|
so1 = make_sales_order(item_code=item, qty=1)
|
||||||
|
|
||||||
|
pln = frappe.new_doc("Production Plan")
|
||||||
|
pln.company = so1.company
|
||||||
|
pln.get_items_from = "Sales Order"
|
||||||
|
pln.append(
|
||||||
|
"sales_orders",
|
||||||
|
{
|
||||||
|
"sales_order": so1.name,
|
||||||
|
"sales_order_date": so1.transaction_date,
|
||||||
|
"customer": so1.customer,
|
||||||
|
"grand_total": so1.grand_total,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pln.get_items()
|
||||||
|
|
||||||
|
self.assertFalse(pln.po_items)
|
||||||
|
|
||||||
def test_production_plan_combine_items(self):
|
def test_production_plan_combine_items(self):
|
||||||
"Test combining FG items in Production Plan."
|
"Test combining FG items in Production Plan."
|
||||||
item = "Test Production Item 1"
|
item = "Test Production Item 1"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "FG Warehouse",
|
"label": "Finished Goods Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:20.252166",
|
"modified": "2024-06-03 13:10:20.252166",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Item",
|
"name": "Production Plan Item",
|
||||||
|
|||||||
@@ -149,13 +149,7 @@ frappe.ui.form.on("Work Order", {
|
|||||||
frm.doc.operations &&
|
frm.doc.operations &&
|
||||||
frm.doc.operations.length
|
frm.doc.operations.length
|
||||||
) {
|
) {
|
||||||
const not_completed = frm.doc.operations.filter((d) => {
|
if (frm.doc.__onload?.show_create_job_card_button) {
|
||||||
if (d.status != "Completed") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (not_completed && not_completed.length) {
|
|
||||||
frm.add_custom_button(__("Create Job Card"), () => {
|
frm.add_custom_button(__("Create Job Card"), () => {
|
||||||
frm.trigger("make_job_card");
|
frm.trigger("make_job_card");
|
||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
@@ -277,6 +271,18 @@ frappe.ui.form.on("Work Order", {
|
|||||||
label: __("Sequence Id"),
|
label: __("Sequence Id"),
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Check",
|
||||||
|
fieldname: "skip_material_transfer",
|
||||||
|
label: __("Skip Material Transfer"),
|
||||||
|
read_only: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Check",
|
||||||
|
fieldname: "backflush_from_wip_warehouse",
|
||||||
|
label: __("Backflush Materials From WIP Warehouse"),
|
||||||
|
read_only: 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
data: operations_data,
|
data: operations_data,
|
||||||
in_place_edit: true,
|
in_place_edit: true,
|
||||||
@@ -317,6 +323,8 @@ frappe.ui.form.on("Work Order", {
|
|||||||
qty: pending_qty,
|
qty: pending_qty,
|
||||||
pending_qty: pending_qty,
|
pending_qty: pending_qty,
|
||||||
sequence_id: data.sequence_id,
|
sequence_id: data.sequence_id,
|
||||||
|
skip_material_transfer: data.skip_material_transfer,
|
||||||
|
backflush_from_wip_warehouse: data.backflush_from_wip_warehouse,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -615,22 +623,25 @@ erpnext.work_order = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const show_start_btn =
|
if (!frm.doc.track_semi_finished_goods) {
|
||||||
frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1;
|
const show_start_btn =
|
||||||
|
frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1;
|
||||||
|
|
||||||
if (show_start_btn) {
|
if (show_start_btn) {
|
||||||
let pending_to_transfer = frm.doc.required_items.some(
|
let pending_to_transfer = frm.doc.required_items.some(
|
||||||
(item) => flt(item.transferred_qty) < flt(item.required_qty)
|
(item) => flt(item.transferred_qty) < flt(item.required_qty)
|
||||||
);
|
);
|
||||||
if (pending_to_transfer && frm.doc.status != "Stopped") {
|
if (pending_to_transfer && frm.doc.status != "Stopped") {
|
||||||
frm.has_start_btn = true;
|
frm.has_start_btn = true;
|
||||||
frm.add_custom_button(__("Create Pick List"), function () {
|
frm.add_custom_button(__("Create Pick List"), function () {
|
||||||
erpnext.work_order.create_pick_list(frm);
|
erpnext.work_order.create_pick_list(frm);
|
||||||
});
|
});
|
||||||
var start_btn = frm.add_custom_button(__("Start"), function () {
|
|
||||||
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
|
var start_btn = frm.add_custom_button(__("Start"), function () {
|
||||||
});
|
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
|
||||||
start_btn.addClass("btn-primary");
|
});
|
||||||
|
start_btn.addClass("btn-primary");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,16 @@
|
|||||||
"produced_qty",
|
"produced_qty",
|
||||||
"process_loss_qty",
|
"process_loss_qty",
|
||||||
"project",
|
"project",
|
||||||
|
"track_semi_finished_goods",
|
||||||
|
"warehouses",
|
||||||
|
"source_warehouse",
|
||||||
|
"wip_warehouse",
|
||||||
|
"column_break_12",
|
||||||
|
"fg_warehouse",
|
||||||
|
"scrap_warehouse",
|
||||||
|
"operations_section",
|
||||||
|
"transfer_material_against",
|
||||||
|
"operations",
|
||||||
"section_break_ndpq",
|
"section_break_ndpq",
|
||||||
"required_items",
|
"required_items",
|
||||||
"work_order_configuration",
|
"work_order_configuration",
|
||||||
@@ -32,22 +42,11 @@
|
|||||||
"skip_transfer",
|
"skip_transfer",
|
||||||
"from_wip_warehouse",
|
"from_wip_warehouse",
|
||||||
"update_consumed_material_cost_in_project",
|
"update_consumed_material_cost_in_project",
|
||||||
"warehouses",
|
|
||||||
"source_warehouse",
|
|
||||||
"wip_warehouse",
|
|
||||||
"column_break_12",
|
|
||||||
"fg_warehouse",
|
|
||||||
"scrap_warehouse",
|
|
||||||
"serial_no_and_batch_for_finished_good_section",
|
"serial_no_and_batch_for_finished_good_section",
|
||||||
"has_serial_no",
|
"has_serial_no",
|
||||||
"has_batch_no",
|
"has_batch_no",
|
||||||
"column_break_18",
|
"column_break_18",
|
||||||
"batch_size",
|
"batch_size",
|
||||||
"required_items_section",
|
|
||||||
"materials_and_operations_tab",
|
|
||||||
"operations_section",
|
|
||||||
"transfer_material_against",
|
|
||||||
"operations",
|
|
||||||
"time",
|
"time",
|
||||||
"planned_start_date",
|
"planned_start_date",
|
||||||
"planned_end_date",
|
"planned_end_date",
|
||||||
@@ -196,7 +195,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0",
|
"depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0 && doc.track_semi_finished_goods === 0",
|
||||||
"fieldname": "material_transferred_for_manufacturing",
|
"fieldname": "material_transferred_for_manufacturing",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Material Transferred for Manufacturing",
|
"label": "Material Transferred for Manufacturing",
|
||||||
@@ -248,7 +247,7 @@
|
|||||||
"fieldname": "wip_warehouse",
|
"fieldname": "wip_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Work-in-Progress Warehouse",
|
"label": "Work-in-Progress Warehouse",
|
||||||
"mandatory_depends_on": "eval:!doc.skip_transfer || doc.from_wip_warehouse",
|
"mandatory_depends_on": "eval:(!doc.skip_transfer || doc.from_wip_warehouse) && !doc.track_semi_finished_goods",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -256,8 +255,7 @@
|
|||||||
"fieldname": "fg_warehouse",
|
"fieldname": "fg_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Target Warehouse",
|
"label": "Target Warehouse",
|
||||||
"options": "Warehouse",
|
"options": "Warehouse"
|
||||||
"reqd": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_12",
|
"fieldname": "column_break_12",
|
||||||
@@ -270,15 +268,9 @@
|
|||||||
"label": "Scrap Warehouse",
|
"label": "Scrap Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "required_items_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Required Items"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "required_items",
|
"fieldname": "required_items",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Required Items",
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Work Order Item",
|
"options": "Work Order Item",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
@@ -336,7 +328,7 @@
|
|||||||
"options": "fa fa-wrench"
|
"options": "fa fa-wrench"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "operations",
|
"depends_on": "eval: doc.operations?.length && doc.track_semi_finished_goods === 0",
|
||||||
"fetch_from": "bom_no.transfer_material_against",
|
"fetch_from": "bom_no.transfer_material_against",
|
||||||
"fetch_if_empty": 1,
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "transfer_material_against",
|
"fieldname": "transfer_material_against",
|
||||||
@@ -579,13 +571,19 @@
|
|||||||
"label": "Configuration"
|
"label": "Configuration"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "materials_and_operations_tab",
|
"collapsible": 1,
|
||||||
"fieldtype": "Tab Break",
|
"collapsible_depends_on": "eval:!doc.operations?.length",
|
||||||
"label": "Operations"
|
"fieldname": "section_break_ndpq",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Required Items"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_ndpq",
|
"default": "0",
|
||||||
"fieldtype": "Section Break"
|
"fetch_from": "bom_no.track_semi_finished_goods",
|
||||||
|
"fieldname": "track_semi_finished_goods",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Track Semi Finished Goods",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cogs",
|
"icon": "fa fa-cogs",
|
||||||
@@ -593,7 +591,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:11:00.129434",
|
"modified": "2024-03-27 13:13:00.129434",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order",
|
"name": "Work Order",
|
||||||
|
|||||||
@@ -143,6 +143,24 @@ class WorkOrder(Document):
|
|||||||
self.set_onload("material_consumption", ms.material_consumption)
|
self.set_onload("material_consumption", ms.material_consumption)
|
||||||
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
|
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
|
||||||
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
|
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
|
||||||
|
self.set_onload("show_create_job_card_button", self.show_create_job_card_button())
|
||||||
|
|
||||||
|
def show_create_job_card_button(self):
|
||||||
|
operation_details = frappe._dict(
|
||||||
|
frappe.get_all(
|
||||||
|
"Job Card",
|
||||||
|
fields=["operation", "for_quantity"],
|
||||||
|
filters={"docstatus": ("<", 2), "work_order": self.name},
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in self.operations:
|
||||||
|
job_card_qty = self.qty - flt(operation_details.get(d.operation))
|
||||||
|
if job_card_qty > 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_production_item()
|
self.validate_production_item()
|
||||||
@@ -422,15 +440,20 @@ class WorkOrder(Document):
|
|||||||
self.update_status()
|
self.update_status()
|
||||||
production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item)
|
production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item)
|
||||||
|
|
||||||
|
def validate_warehouse(self):
|
||||||
|
if self.track_semi_finished_goods:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.wip_warehouse and not self.skip_transfer:
|
||||||
|
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
|
||||||
|
if not self.fg_warehouse:
|
||||||
|
frappe.throw(_("Target Warehouse is required before Submit"))
|
||||||
|
|
||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
self.create_serial_no_batch_no()
|
self.create_serial_no_batch_no()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if not self.wip_warehouse and not self.skip_transfer:
|
self.validate_warehouse()
|
||||||
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
|
|
||||||
if not self.fg_warehouse:
|
|
||||||
frappe.throw(_("For Warehouse is required before Submit"))
|
|
||||||
|
|
||||||
if self.production_plan and frappe.db.exists(
|
if self.production_plan and frappe.db.exists(
|
||||||
"Production Plan Item Reference", {"parent": self.production_plan}
|
"Production Plan Item Reference", {"parent": self.production_plan}
|
||||||
):
|
):
|
||||||
@@ -667,6 +690,9 @@ class WorkOrder(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_planned_qty(self):
|
def update_planned_qty(self):
|
||||||
|
if self.track_semi_finished_goods:
|
||||||
|
return
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||||
get_reserved_qty_for_sub_assembly,
|
get_reserved_qty_for_sub_assembly,
|
||||||
)
|
)
|
||||||
@@ -811,13 +837,21 @@ class WorkOrder(Document):
|
|||||||
"description",
|
"description",
|
||||||
"workstation",
|
"workstation",
|
||||||
"idx",
|
"idx",
|
||||||
|
"finished_good",
|
||||||
|
"is_subcontracted",
|
||||||
|
"wip_warehouse",
|
||||||
|
"source_warehouse",
|
||||||
|
"fg_warehouse",
|
||||||
"workstation_type",
|
"workstation_type",
|
||||||
"base_hour_rate as hour_rate",
|
"base_hour_rate as hour_rate",
|
||||||
"time_in_mins",
|
"time_in_mins",
|
||||||
"parent as bom",
|
"parent as bom",
|
||||||
|
"bom_no",
|
||||||
"batch_size",
|
"batch_size",
|
||||||
"sequence_id",
|
"sequence_id",
|
||||||
"fixed_time",
|
"fixed_time",
|
||||||
|
"skip_material_transfer",
|
||||||
|
"backflush_from_wip_warehouse",
|
||||||
],
|
],
|
||||||
order_by="idx",
|
order_by="idx",
|
||||||
)
|
)
|
||||||
@@ -827,6 +861,9 @@ class WorkOrder(Document):
|
|||||||
d.time_in_mins = flt(d.time_in_mins) * flt(qty)
|
d.time_in_mins = flt(d.time_in_mins) * flt(qty)
|
||||||
d.status = "Pending"
|
d.status = "Pending"
|
||||||
|
|
||||||
|
if self.track_semi_finished_goods and not d.sequence_id:
|
||||||
|
d.sequence_id = d.idx
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
self.set("operations", [])
|
self.set("operations", [])
|
||||||
@@ -1084,6 +1121,7 @@ class WorkOrder(Document):
|
|||||||
"required_qty": item.qty,
|
"required_qty": item.qty,
|
||||||
"source_warehouse": item.source_warehouse or item.default_warehouse,
|
"source_warehouse": item.source_warehouse or item.default_warehouse,
|
||||||
"include_item_in_manufacturing": item.include_item_in_manufacturing,
|
"include_item_in_manufacturing": item.include_item_in_manufacturing,
|
||||||
|
"operation_row_id": item.operation_row_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1284,6 +1322,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None):
|
|||||||
item_details = get_item_details(item, project)
|
item_details = get_item_details(item, project)
|
||||||
|
|
||||||
wo_doc = frappe.new_doc("Work Order")
|
wo_doc = frappe.new_doc("Work Order")
|
||||||
|
wo_doc.track_semi_finished_goods = frappe.db.get_value("BOM", bom_no, "track_semi_finished_goods")
|
||||||
wo_doc.production_item = item
|
wo_doc.production_item = item
|
||||||
wo_doc.update(item_details)
|
wo_doc.update(item_details)
|
||||||
wo_doc.bom_no = bom_no
|
wo_doc.bom_no = bom_no
|
||||||
@@ -1450,6 +1489,8 @@ def make_job_card(work_order, 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)
|
row = frappe._dict(row)
|
||||||
|
row.update(get_operation_details(row.name, work_order))
|
||||||
|
|
||||||
validate_operation_data(row)
|
validate_operation_data(row)
|
||||||
qty = row.get("qty")
|
qty = row.get("qty")
|
||||||
while qty > 0:
|
while qty > 0:
|
||||||
@@ -1458,6 +1499,21 @@ def make_job_card(work_order, operations):
|
|||||||
create_job_card(work_order, row, auto_create=True)
|
create_job_card(work_order, row, auto_create=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_operation_details(name, work_order):
|
||||||
|
for row in work_order.operations:
|
||||||
|
if row.name == name:
|
||||||
|
return {
|
||||||
|
"workstation": row.workstation,
|
||||||
|
"workstation_type": row.workstation_type,
|
||||||
|
"source_warehouse": row.source_warehouse,
|
||||||
|
"fg_warehouse": row.fg_warehouse,
|
||||||
|
"wip_warehouse": row.wip_warehouse,
|
||||||
|
"finished_good": row.finished_good,
|
||||||
|
"bom_no": row.get("bom_no"),
|
||||||
|
"is_subcontracted": row.get("is_subcontracted"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def close_work_order(work_order, status):
|
def close_work_order(work_order, status):
|
||||||
if not frappe.has_permission("Work Order", "write"):
|
if not frappe.has_permission("Work Order", "write"):
|
||||||
@@ -1558,6 +1614,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
|||||||
"workstation_type": row.get("workstation_type"),
|
"workstation_type": row.get("workstation_type"),
|
||||||
"operation": row.get("operation"),
|
"operation": row.get("operation"),
|
||||||
"workstation": row.get("workstation"),
|
"workstation": row.get("workstation"),
|
||||||
|
"operation_row_id": cint(row.idx),
|
||||||
"posting_date": nowdate(),
|
"posting_date": nowdate(),
|
||||||
"for_quantity": row.job_card_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"),
|
||||||
@@ -1565,13 +1622,22 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
|||||||
"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,
|
|
||||||
"hour_rate": row.get("hour_rate"),
|
"hour_rate": row.get("hour_rate"),
|
||||||
"serial_no": row.get("serial_no"),
|
"serial_no": row.get("serial_no"),
|
||||||
|
"source_warehouse": row.get("source_warehouse"),
|
||||||
|
"target_warehouse": row.get("fg_warehouse"),
|
||||||
|
"wip_warehouse": work_order.wip_warehouse or row.get("wip_warehouse"),
|
||||||
|
"skip_material_transfer": row.get("skip_material_transfer"),
|
||||||
|
"backflush_from_wip_warehouse": row.get("backflush_from_wip_warehouse"),
|
||||||
|
"finished_good": row.get("finished_good"),
|
||||||
|
"semi_fg_bom": row.get("bom_no"),
|
||||||
|
"is_subcontracted": row.get("is_subcontracted"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer:
|
if work_order.track_semi_finished_goods or (
|
||||||
|
work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer
|
||||||
|
):
|
||||||
doc.get_required_items()
|
doc.get_required_items()
|
||||||
|
|
||||||
if auto_create:
|
if auto_create:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"operation",
|
"operation",
|
||||||
"item_code",
|
"item_code",
|
||||||
"source_warehouse",
|
"source_warehouse",
|
||||||
|
"operation_row_id",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"item_name",
|
"item_name",
|
||||||
"description",
|
"description",
|
||||||
@@ -138,11 +139,17 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Returned Qty ",
|
"label": "Returned Qty ",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "operation_row_id",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Operation Row Id",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:11:00.429838",
|
"modified": "2024-03-27 13:12:00.429838",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order Item",
|
"name": "Work Order Item",
|
||||||
|
|||||||
@@ -15,6 +15,16 @@
|
|||||||
"workstation_type",
|
"workstation_type",
|
||||||
"workstation",
|
"workstation",
|
||||||
"sequence_id",
|
"sequence_id",
|
||||||
|
"section_break_insy",
|
||||||
|
"bom_no",
|
||||||
|
"finished_good",
|
||||||
|
"is_subcontracted",
|
||||||
|
"skip_material_transfer",
|
||||||
|
"backflush_from_wip_warehouse",
|
||||||
|
"column_break_vjih",
|
||||||
|
"source_warehouse",
|
||||||
|
"wip_warehouse",
|
||||||
|
"fg_warehouse",
|
||||||
"section_break_10",
|
"section_break_10",
|
||||||
"description",
|
"description",
|
||||||
"estimated_time_and_cost",
|
"estimated_time_and_cost",
|
||||||
@@ -52,7 +62,6 @@
|
|||||||
"columns": 2,
|
"columns": 2,
|
||||||
"fieldname": "bom",
|
"fieldname": "bom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "BOM",
|
"label": "BOM",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "BOM",
|
"options": "BOM",
|
||||||
@@ -66,11 +75,10 @@
|
|||||||
"oldfieldtype": "Text"
|
"oldfieldtype": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 2,
|
"columns": 1,
|
||||||
"description": "Operation completed for how many finished goods?",
|
"description": "Operation completed for how many finished goods?",
|
||||||
"fieldname": "completed_qty",
|
"fieldname": "completed_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Completed Qty",
|
"label": "Completed Qty",
|
||||||
"no_copy": 1
|
"no_copy": 1
|
||||||
},
|
},
|
||||||
@@ -213,16 +221,83 @@
|
|||||||
"columns": 2,
|
"columns": 2,
|
||||||
"fieldname": "process_loss_qty",
|
"fieldname": "process_loss_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Process Loss Qty",
|
"label": "Process Loss Qty",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:parent.track_semi_finished_goods === 1",
|
||||||
|
"fieldname": "section_break_insy",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "bom_no",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "BOM No (For Semi-Finished Goods)",
|
||||||
|
"options": "BOM",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_vjih",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "finished_good",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Semi Finished Goods / Finished Goods",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "wip_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "WIP WH",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "fg_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Finished Goods Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "source_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Source Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_subcontracted",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Subcontracted",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "skip_material_transfer",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Skip Material Transfer",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "backflush_from_wip_warehouse",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Backflush Materials From WIP Warehouse",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:11:00.595376",
|
"modified": "2024-06-03 15:57:17.958543",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order Operation",
|
"name": "Work Order Operation",
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ class WorkOrderOperation(Document):
|
|||||||
actual_operating_cost: DF.Currency
|
actual_operating_cost: DF.Currency
|
||||||
actual_operation_time: DF.Float
|
actual_operation_time: DF.Float
|
||||||
actual_start_time: DF.Datetime | None
|
actual_start_time: DF.Datetime | None
|
||||||
|
backflush_from_wip_warehouse: DF.Check
|
||||||
batch_size: DF.Float
|
batch_size: DF.Float
|
||||||
bom: DF.Link | None
|
bom: DF.Link | None
|
||||||
|
bom_no: DF.Link | None
|
||||||
completed_qty: DF.Float
|
completed_qty: DF.Float
|
||||||
description: DF.TextEditor | None
|
description: DF.TextEditor | None
|
||||||
|
fg_warehouse: DF.Link | None
|
||||||
|
finished_good: DF.Link | None
|
||||||
hour_rate: DF.Float
|
hour_rate: DF.Float
|
||||||
|
is_subcontracted: DF.Check
|
||||||
operation: DF.Link
|
operation: DF.Link
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
@@ -32,8 +37,11 @@ class WorkOrderOperation(Document):
|
|||||||
planned_start_time: DF.Datetime | None
|
planned_start_time: DF.Datetime | None
|
||||||
process_loss_qty: DF.Float
|
process_loss_qty: DF.Float
|
||||||
sequence_id: DF.Int
|
sequence_id: DF.Int
|
||||||
|
skip_material_transfer: DF.Check
|
||||||
|
source_warehouse: DF.Link | None
|
||||||
status: DF.Literal["Pending", "Work in Progress", "Completed"]
|
status: DF.Literal["Pending", "Work in Progress", "Completed"]
|
||||||
time_in_mins: DF.Float
|
time_in_mins: DF.Float
|
||||||
|
wip_warehouse: DF.Link | None
|
||||||
workstation: DF.Link | None
|
workstation: DF.Link | None
|
||||||
workstation_type: DF.Link | None
|
workstation_type: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -105,16 +105,118 @@ class WorkstationDashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_job_cards() {
|
render_job_cards() {
|
||||||
let template = frappe.render_template("workstation_job_card", {
|
this.template = frappe.render_template("workstation_job_card", {
|
||||||
data: this.job_cards,
|
data: this.job_cards,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$wrapper.html(template);
|
this.timer_job_cards = {};
|
||||||
|
this.$wrapper.html(this.template);
|
||||||
|
this.setup_qrcode_fields();
|
||||||
this.prepare_timer();
|
this.prepare_timer();
|
||||||
|
this.setup_menu_actions();
|
||||||
this.toggle_job_card();
|
this.toggle_job_card();
|
||||||
this.bind_events();
|
this.bind_events();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setup_qrcode_fields() {
|
||||||
|
this.start_job_qrcode = frappe.ui.form.make_control({
|
||||||
|
df: {
|
||||||
|
label: __("Start Job"),
|
||||||
|
fieldtype: "Data",
|
||||||
|
options: "Barcode",
|
||||||
|
placeholder: __("Scan Job Card Qrcode"),
|
||||||
|
},
|
||||||
|
parent: this.$wrapper.find(".qrcode-fields"),
|
||||||
|
render_input: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.start_job_qrcode.$wrapper.addClass("form-column col-sm-6");
|
||||||
|
|
||||||
|
this.start_job_qrcode.$input.on("input", (e) => {
|
||||||
|
clearTimeout(this.start_job_qrcode_search);
|
||||||
|
this.start_job_qrcode_search = setTimeout(() => {
|
||||||
|
let job_card = this.start_job_qrcode.get_value();
|
||||||
|
if (job_card) {
|
||||||
|
this.validate_job_card(job_card, "Open", (job_card, qty) => {
|
||||||
|
this.start_job(job_card);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.start_job_qrcode.set_value("");
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.complete_job_qrcode = frappe.ui.form.make_control({
|
||||||
|
df: {
|
||||||
|
label: __("Complete Job"),
|
||||||
|
fieldtype: "Data",
|
||||||
|
options: "Barcode",
|
||||||
|
placeholder: __("Scan Job Card Qrcode"),
|
||||||
|
},
|
||||||
|
parent: this.$wrapper.find(".qrcode-fields"),
|
||||||
|
render_input: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.complete_job_qrcode.$input.on("input", (e) => {
|
||||||
|
clearTimeout(this.complete_job_qrcode_search);
|
||||||
|
this.complete_job_qrcode_search = setTimeout(() => {
|
||||||
|
let job_card = this.complete_job_qrcode.get_value();
|
||||||
|
if (job_card) {
|
||||||
|
this.validate_job_card(job_card, "Work In Progress", (job_card, qty) => {
|
||||||
|
this.complete_job(job_card, qty);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.complete_job_qrcode.set_value("");
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.complete_job_qrcode.$wrapper.addClass("form-column col-sm-6");
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_job_card(job_card, status, callback) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.manufacturing.doctype.workstation.workstation.validate_job_card",
|
||||||
|
args: {
|
||||||
|
job_card: job_card,
|
||||||
|
status: status,
|
||||||
|
},
|
||||||
|
callback(r) {
|
||||||
|
callback(job_card, r.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_menu_actions() {
|
||||||
|
let me = this;
|
||||||
|
this.job_cards.forEach((data) => {
|
||||||
|
me.menu_actions = me.$wrapper.find(`.menu-actions[data-job-card='${data.name}']`);
|
||||||
|
$(me.menu_actions).find(".btn-start").hide();
|
||||||
|
$(me.menu_actions).find(".btn-resume").hide();
|
||||||
|
$(me.menu_actions).find(".btn-pause").hide();
|
||||||
|
$(me.menu_actions).find(".btn-complete").hide();
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.for_quantity + data.process_loss_qty > data.total_completed_qty &&
|
||||||
|
(data.skip_material_transfer ||
|
||||||
|
data.transferred_qty >= data.for_quantity + data.process_loss_qty ||
|
||||||
|
!data.finished_good)
|
||||||
|
) {
|
||||||
|
if (!data.time_logs?.length) {
|
||||||
|
$(me.menu_actions).find(".btn-start").show();
|
||||||
|
} else if (data.is_paused) {
|
||||||
|
$(me.menu_actions).find(".btn-resume").show();
|
||||||
|
} else if (data.for_quantity - data.manufactured_qty > 0) {
|
||||||
|
if (!data.is_paused) {
|
||||||
|
$(me.menu_actions).find(".btn-pause").show();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(me.menu_actions).find(".btn-complete").show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toggle_job_card() {
|
toggle_job_card() {
|
||||||
this.$wrapper.find(".collapse-indicator-job").on("click", (e) => {
|
this.$wrapper.find(".collapse-indicator-job").on("click", (e) => {
|
||||||
$(e.currentTarget)
|
$(e.currentTarget)
|
||||||
@@ -133,106 +235,75 @@ class WorkstationDashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bind_events() {
|
bind_events() {
|
||||||
this.$wrapper.find(".make-material-request").on("click", (e) => {
|
let me = this;
|
||||||
let job_card = $(e.currentTarget).attr("job-card");
|
|
||||||
|
this.$wrapper.find(".btn-transfer-materials").on("click", (e) => {
|
||||||
|
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
|
||||||
this.make_material_request(job_card);
|
this.make_material_request(job_card);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$wrapper.find(".btn-start").on("click", (e) => {
|
this.$wrapper.find(".btn-start").on("click", (e) => {
|
||||||
let job_card = $(e.currentTarget).attr("job-card");
|
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
|
||||||
this.start_job(job_card);
|
this.start_job(job_card);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$wrapper.find(".btn-pause").on("click", (e) => {
|
||||||
|
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
|
||||||
|
me.update_job_card(job_card, "pause_job", {
|
||||||
|
end_time: frappe.datetime.now_datetime(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$wrapper.find(".btn-resume").on("click", (e) => {
|
||||||
|
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
|
||||||
|
me.update_job_card(job_card, "resume_job", {
|
||||||
|
start_time: frappe.datetime.now_datetime(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.$wrapper.find(".btn-complete").on("click", (e) => {
|
this.$wrapper.find(".btn-complete").on("click", (e) => {
|
||||||
let job_card = $(e.currentTarget).attr("job-card");
|
let job_card = $(e.currentTarget).closest("ul").attr("data-job-card");
|
||||||
let pending_qty = flt($(e.currentTarget).attr("pending-qty"));
|
let for_quantity = $(e.currentTarget).attr("data-qty");
|
||||||
this.complete_job(job_card, pending_qty);
|
me.complete_job(job_card, for_quantity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
start_job(job_card) {
|
start_job(job_card) {
|
||||||
let me = this;
|
let me = this;
|
||||||
frappe.prompt(
|
|
||||||
[
|
let fields = this.get_fields_for_employee();
|
||||||
{
|
|
||||||
fieldtype: "Datetime",
|
this.employee_dialog = frappe.prompt(fields, (values) => {
|
||||||
label: __("Start Time"),
|
me.update_job_card(job_card, "start_timer", values);
|
||||||
fieldname: "start_time",
|
});
|
||||||
reqd: 1,
|
|
||||||
default: frappe.datetime.now_datetime(),
|
let default_employee = this.job_cards[0]?.user_employee;
|
||||||
},
|
if (default_employee) {
|
||||||
{
|
this.employee_dialog.fields_dict.employees.df.data.push({
|
||||||
label: __("Operator"),
|
employee: default_employee,
|
||||||
fieldname: "employee",
|
});
|
||||||
fieldtype: "Link",
|
this.employee_dialog.fields_dict.employees.grid.refresh();
|
||||||
options: "Employee",
|
}
|
||||||
},
|
|
||||||
],
|
|
||||||
(data) => {
|
|
||||||
this.frm.call({
|
|
||||||
method: "start_job",
|
|
||||||
doc: this.frm.doc,
|
|
||||||
args: {
|
|
||||||
job_card: job_card,
|
|
||||||
from_time: data.start_time,
|
|
||||||
employee: data.employee,
|
|
||||||
},
|
|
||||||
callback(r) {
|
|
||||||
if (r.message) {
|
|
||||||
me.job_cards = [r.message];
|
|
||||||
me.prepare_timer();
|
|
||||||
me.update_job_card_details();
|
|
||||||
me.frm.reload_doc();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
__("Enter Value"),
|
|
||||||
__("Start Job")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
complete_job(job_card, qty_to_manufacture) {
|
complete_job(job_card, for_quantity) {
|
||||||
let me = this;
|
|
||||||
let fields = [
|
|
||||||
{
|
|
||||||
fieldtype: "Float",
|
|
||||||
label: __("Completed Quantity"),
|
|
||||||
fieldname: "qty",
|
|
||||||
reqd: 1,
|
|
||||||
default: flt(qty_to_manufacture || 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Datetime",
|
|
||||||
label: __("End Time"),
|
|
||||||
fieldname: "end_time",
|
|
||||||
default: frappe.datetime.now_datetime(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
frappe.prompt(
|
frappe.prompt(
|
||||||
fields,
|
{
|
||||||
|
fieldname: "qty",
|
||||||
|
label: __("Completed Quantity"),
|
||||||
|
fieldtype: "Float",
|
||||||
|
reqd: 1,
|
||||||
|
default: flt(for_quantity || 0),
|
||||||
|
},
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.qty <= 0) {
|
if (flt(data.qty) <= 0) {
|
||||||
frappe.throw(__("Quantity should be greater than 0"));
|
frappe.throw(__("Quantity should be greater than 0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.call({
|
this.update_job_card(job_card, "complete_job_card", {
|
||||||
method: "complete_job",
|
qty: flt(data.qty),
|
||||||
doc: this.frm.doc,
|
end_time: frappe.datetime.now_datetime(),
|
||||||
args: {
|
auto_submit: 1,
|
||||||
job_card: job_card,
|
|
||||||
qty: data.qty,
|
|
||||||
to_time: data.end_time,
|
|
||||||
},
|
|
||||||
callback: function (r) {
|
|
||||||
if (r.message) {
|
|
||||||
me.job_cards = [r.message];
|
|
||||||
me.prepare_timer();
|
|
||||||
me.update_job_card_details();
|
|
||||||
me.frm.reload_doc();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
__("Enter Value"),
|
__("Enter Value"),
|
||||||
@@ -240,26 +311,219 @@ class WorkstationDashboard {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
make_material_request(job_card) {
|
get_fields_for_employee() {
|
||||||
|
let me = this;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __("Employee"),
|
||||||
|
fieldname: "employee",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Employee",
|
||||||
|
change() {
|
||||||
|
let employee = this.get_value();
|
||||||
|
let employees = me.employee_dialog.fields_dict.employees.df.data;
|
||||||
|
|
||||||
|
if (employee) {
|
||||||
|
let employee_exists = employees.find((d) => d.employee === employee);
|
||||||
|
|
||||||
|
if (!employee_exists) {
|
||||||
|
me.employee_dialog.fields_dict.employees.df.data.push({
|
||||||
|
employee: employee,
|
||||||
|
});
|
||||||
|
|
||||||
|
me.employee_dialog.fields_dict.employees.grid.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Start Time"),
|
||||||
|
fieldname: "start_time",
|
||||||
|
fieldtype: "Datetime",
|
||||||
|
default: frappe.datetime.now_datetime(),
|
||||||
|
},
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
|
{
|
||||||
|
label: __("Employees"),
|
||||||
|
fieldname: "employees",
|
||||||
|
fieldtype: "Table",
|
||||||
|
data: [],
|
||||||
|
cannot_add_rows: 1,
|
||||||
|
cannot_delete_rows: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: __("Employee"),
|
||||||
|
fieldname: "employee",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Employee",
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
update_job_card(job_card, method, data) {
|
||||||
|
let me = this;
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request",
|
method: "erpnext.manufacturing.doctype.workstation.workstation.update_job_card",
|
||||||
args: {
|
args: {
|
||||||
source_name: job_card,
|
job_card: job_card,
|
||||||
|
method: method,
|
||||||
|
start_time: data.start_time || "",
|
||||||
|
employees: data.employees || [],
|
||||||
|
end_time: data.end_time || "",
|
||||||
|
qty: data.qty || 0,
|
||||||
|
auto_submit: data.auto_submit || 0,
|
||||||
|
},
|
||||||
|
callback: () => {
|
||||||
|
$.each(me.timer_job_cards, (index, value) => {
|
||||||
|
clearInterval(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
me.frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
make_material_request(job_card) {
|
||||||
|
let me = this;
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.manufacturing.doctype.workstation.workstation.get_raw_materials",
|
||||||
|
args: {
|
||||||
|
job_card: job_card,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
var doc = frappe.model.sync(r.message)[0];
|
me.prepare_materials_modal(r.message, job_card, (job_card) => {
|
||||||
frappe.set_route("Form", doc.doctype, doc.name);
|
frappe.call({
|
||||||
|
method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry",
|
||||||
|
args: {
|
||||||
|
source_name: job_card,
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
var doc = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepare_materials_modal(raw_materials, job_card, callback) {
|
||||||
|
let fields = this.get_raw_material_fields(raw_materials);
|
||||||
|
|
||||||
|
this.materials_dialog = new frappe.ui.Dialog({
|
||||||
|
title: "Raw Materials",
|
||||||
|
fields: fields,
|
||||||
|
size: "large",
|
||||||
|
primary_action_label: __("Make Transfer Entry"),
|
||||||
|
primary_action: () => {
|
||||||
|
this.materials_dialog.hide();
|
||||||
|
callback(job_card);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
raw_materials.forEach((row) => {
|
||||||
|
this.materials_dialog.fields_dict.items.df.data.push(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.materials_dialog.fields_dict.items.grid.refresh();
|
||||||
|
this.materials_dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
get_raw_material_fields(raw_materials) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __("Warehouse"),
|
||||||
|
fieldname: "warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Warehouse",
|
||||||
|
read_only: 1,
|
||||||
|
default: raw_materials[0].warehouse,
|
||||||
|
},
|
||||||
|
{ fieldtype: "Column Break" },
|
||||||
|
{
|
||||||
|
label: __("Skip Material Transfer"),
|
||||||
|
fieldname: "skip_material_transfer",
|
||||||
|
fieldtype: "Check",
|
||||||
|
read_only: 1,
|
||||||
|
default: raw_materials[0].skip_material_transfer,
|
||||||
|
},
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
|
{
|
||||||
|
label: __("Raw Materials"),
|
||||||
|
fieldname: "items",
|
||||||
|
fieldtype: "Table",
|
||||||
|
cannot_add_rows: 1,
|
||||||
|
cannot_delete_rows: 1,
|
||||||
|
data: [],
|
||||||
|
size: "extra-large",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: __("Item Code"),
|
||||||
|
fieldname: "item_code",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item",
|
||||||
|
in_list_view: 1,
|
||||||
|
read_only: 1,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("UOM"),
|
||||||
|
fieldname: "uom",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "UOM",
|
||||||
|
in_list_view: 1,
|
||||||
|
read_only: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Reqired Qty"),
|
||||||
|
fieldname: "required_qty",
|
||||||
|
fieldtype: "Float",
|
||||||
|
in_list_view: 1,
|
||||||
|
read_only: 1,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Transferred Qty"),
|
||||||
|
fieldname: "transferred_qty",
|
||||||
|
fieldtype: "Float",
|
||||||
|
in_list_view: 1,
|
||||||
|
read_only: 1,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Available Qty"),
|
||||||
|
fieldname: "stock_qty",
|
||||||
|
fieldtype: "Float",
|
||||||
|
in_list_view: 1,
|
||||||
|
read_only: 1,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Available"),
|
||||||
|
fieldname: "material_availability_status",
|
||||||
|
fieldtype: "Check",
|
||||||
|
in_list_view: 1,
|
||||||
|
read_only: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
prepare_timer() {
|
prepare_timer() {
|
||||||
this.job_cards.forEach((data) => {
|
this.job_cards.forEach((data) => {
|
||||||
if (data.time_logs?.length) {
|
if (data.time_logs?.length) {
|
||||||
data._current_time = this.get_current_time(data);
|
data._current_time = this.get_current_time(data);
|
||||||
if (data.time_logs[cint(data.time_logs.length) - 1].to_time) {
|
if (data.time_logs[cint(data.time_logs.length) - 1].to_time || data.is_paused) {
|
||||||
this.updateStopwatch(data);
|
this.updateStopwatch(data);
|
||||||
} else {
|
} else {
|
||||||
this.initialiseTimer(data);
|
this.initialiseTimer(data);
|
||||||
@@ -283,23 +547,23 @@ class WorkstationDashboard {
|
|||||||
[data-name='${data.name}']`);
|
[data-name='${data.name}']`);
|
||||||
|
|
||||||
$(job_card_selector).find(".job-card-status").text(data.status);
|
$(job_card_selector).find(".job-card-status").text(data.status);
|
||||||
$(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
|
|
||||||
|
|
||||||
if (data.status === "Work In Progress") {
|
["blue", "gray", "green", "orange", "yellow"].forEach((color) => {
|
||||||
$(job_card_selector).find(".btn-start").addClass("hide");
|
$(job_card_selector).find(".job-card-status").removeClass(color);
|
||||||
$(job_card_selector).find(".btn-complete").removeClass("hide");
|
});
|
||||||
} else if (data.status === "Completed") {
|
|
||||||
$(job_card_selector).find(".btn-start").addClass("hide");
|
$(job_card_selector).find(".job-card-status").addClass(data.status_color);
|
||||||
$(job_card_selector).find(".btn-complete").addClass("hide");
|
$(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initialiseTimer(data) {
|
initialiseTimer(data) {
|
||||||
setInterval(() => {
|
let timeout = setInterval(() => {
|
||||||
data._current_time += 1;
|
data._current_time += 1;
|
||||||
this.updateStopwatch(data);
|
this.updateStopwatch(data);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
this.timer_job_cards[data.name] = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStopwatch(data) {
|
updateStopwatch(data) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"dashboard_tab",
|
"dashboard_tab",
|
||||||
|
"section_break_mqqv",
|
||||||
"workstation_dashboard",
|
"workstation_dashboard",
|
||||||
"details_tab",
|
"details_tab",
|
||||||
"workstation_name",
|
"workstation_name",
|
||||||
@@ -246,13 +247,18 @@
|
|||||||
"fieldname": "workstation_dashboard",
|
"fieldname": "workstation_dashboard",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"label": "Workstation Dashboard"
|
"label": "Workstation Dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_mqqv",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-wrench",
|
"icon": "icon-wrench",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"image_field": "on_status_image",
|
"image_field": "on_status_image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:11:00.760717",
|
"modified": "2024-06-01 14:48:47.341354",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Workstation",
|
"name": "Workstation",
|
||||||
|
|||||||
@@ -55,7 +55,14 @@ class Workstation(Document):
|
|||||||
hour_rate_electricity: DF.Currency
|
hour_rate_electricity: DF.Currency
|
||||||
hour_rate_labour: DF.Currency
|
hour_rate_labour: DF.Currency
|
||||||
hour_rate_rent: DF.Currency
|
hour_rate_rent: DF.Currency
|
||||||
|
off_status_image: DF.AttachImage | None
|
||||||
|
on_status_image: DF.AttachImage | None
|
||||||
|
parts_per_hour: DF.Float
|
||||||
|
plant_floor: DF.Link | None
|
||||||
production_capacity: DF.Int
|
production_capacity: DF.Int
|
||||||
|
status: DF.Literal["Production", "Off", "Idle", "Problem", "Maintenance", "Setup"]
|
||||||
|
total_working_hours: DF.Float
|
||||||
|
warehouse: DF.Link | None
|
||||||
working_hours: DF.Table[WorkstationWorkingHour]
|
working_hours: DF.Table[WorkstationWorkingHour]
|
||||||
workstation_name: DF.Data
|
workstation_name: DF.Data
|
||||||
workstation_type: DF.Link | None
|
workstation_type: DF.Link | None
|
||||||
@@ -189,7 +196,7 @@ class Workstation(Document):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_job_cards(workstation):
|
def get_job_cards(workstation, job_card=None):
|
||||||
if frappe.has_permission("Job Card", "read"):
|
if frappe.has_permission("Job Card", "read"):
|
||||||
jc_data = frappe.get_all(
|
jc_data = frappe.get_all(
|
||||||
"Job Card",
|
"Job Card",
|
||||||
@@ -200,15 +207,22 @@ def get_job_cards(workstation):
|
|||||||
"operation",
|
"operation",
|
||||||
"total_completed_qty",
|
"total_completed_qty",
|
||||||
"for_quantity",
|
"for_quantity",
|
||||||
|
"process_loss_qty",
|
||||||
|
"finished_good",
|
||||||
"transferred_qty",
|
"transferred_qty",
|
||||||
"status",
|
"status",
|
||||||
"expected_start_date",
|
"expected_start_date",
|
||||||
"expected_end_date",
|
"expected_end_date",
|
||||||
"time_required",
|
"time_required",
|
||||||
"wip_warehouse",
|
"wip_warehouse",
|
||||||
|
"skip_material_transfer",
|
||||||
|
"backflush_from_wip_warehouse",
|
||||||
|
"is_paused",
|
||||||
|
"manufactured_qty",
|
||||||
],
|
],
|
||||||
filters={
|
filters={
|
||||||
"workstation": workstation,
|
"workstation": workstation,
|
||||||
|
"is_subcontracted": 0,
|
||||||
"docstatus": ("<", 2),
|
"docstatus": ("<", 2),
|
||||||
"status": ["not in", ["Completed", "Stopped"]],
|
"status": ["not in", ["Completed", "Stopped"]],
|
||||||
},
|
},
|
||||||
@@ -216,64 +230,98 @@ def get_job_cards(workstation):
|
|||||||
)
|
)
|
||||||
|
|
||||||
job_cards = [row.name for row in jc_data]
|
job_cards = [row.name for row in jc_data]
|
||||||
raw_materials = get_raw_materials(job_cards)
|
|
||||||
time_logs = get_time_logs(job_cards)
|
time_logs = get_time_logs(job_cards)
|
||||||
|
|
||||||
allow_excess_transfer = frappe.db.get_single_value(
|
allow_excess_transfer = frappe.db.get_single_value(
|
||||||
"Manufacturing Settings", "job_card_excess_transfer"
|
"Manufacturing Settings", "job_card_excess_transfer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}, "name")
|
||||||
|
|
||||||
for row in jc_data:
|
for row in jc_data:
|
||||||
row.progress_percent = (
|
item_code = row.finished_good or row.production_item
|
||||||
flt(row.total_completed_qty / row.for_quantity * 100, 2) if row.for_quantity else 0
|
row.fg_uom = frappe.get_cached_value("Item", item_code, "stock_uom")
|
||||||
)
|
|
||||||
row.progress_title = _("Total completed quantity: {0}").format(row.total_completed_qty)
|
|
||||||
row.status_color = get_status_color(row.status)
|
row.status_color = get_status_color(row.status)
|
||||||
row.job_card_link = get_link_to_form("Job Card", row.name)
|
row.job_card_link = f"""
|
||||||
|
<a class="ellipsis" data-doctype="Job Card" data-name="{row.name}" href="/app/job-card/{row.name}" title="" data-original-title="{row.name}">{row.name}</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
row.operation_link = f"""
|
||||||
|
<a class="ellipsis" data-doctype="Operation" data-name="{row.operation}" href="/app/operation/{row.operation}" title="" data-original-title="{row.operation}">{row.operation}</a>
|
||||||
|
"""
|
||||||
row.work_order_link = get_link_to_form("Work Order", row.work_order)
|
row.work_order_link = get_link_to_form("Work Order", row.work_order)
|
||||||
|
|
||||||
row.raw_materials = raw_materials.get(row.name, [])
|
|
||||||
row.time_logs = time_logs.get(row.name, [])
|
row.time_logs = time_logs.get(row.name, [])
|
||||||
row.make_material_request = False
|
row.make_material_request = False
|
||||||
if row.for_quantity > row.transferred_qty or allow_excess_transfer:
|
if row.for_quantity > row.transferred_qty or allow_excess_transfer:
|
||||||
row.make_material_request = True
|
row.make_material_request = True
|
||||||
|
|
||||||
|
row.user_employee = user_employee
|
||||||
|
|
||||||
return jc_data
|
return jc_data
|
||||||
|
|
||||||
|
|
||||||
def get_status_color(status):
|
def get_status_color(status):
|
||||||
color_map = {
|
color_map = {
|
||||||
"Pending": "var(--bg-blue)",
|
"Pending": "blue",
|
||||||
"In Process": "var(--bg-yellow)",
|
"In Process": "yellow",
|
||||||
"Submitted": "var(--bg-blue)",
|
"Submitted": "blue",
|
||||||
"Open": "var(--bg-gray)",
|
"Open": "gray",
|
||||||
"Closed": "var(--bg-green)",
|
"Closed": "green",
|
||||||
"Work In Progress": "var(--bg-orange)",
|
"Work In Progress": "orange",
|
||||||
}
|
}
|
||||||
|
|
||||||
return color_map.get(status, "var(--bg-blue)")
|
return color_map.get(status, "blue")
|
||||||
|
|
||||||
|
|
||||||
def get_raw_materials(job_cards):
|
@frappe.whitelist()
|
||||||
raw_materials = {}
|
def get_raw_materials(job_card):
|
||||||
|
raw_materials = frappe.get_all(
|
||||||
data = frappe.get_all(
|
"Job Card",
|
||||||
"Job Card Item",
|
|
||||||
fields=[
|
fields=[
|
||||||
"parent",
|
"`tabJob Card`.`skip_material_transfer`",
|
||||||
"item_code",
|
"`tabJob Card`.`backflush_from_wip_warehouse`",
|
||||||
"item_group",
|
"`tabJob Card`.`wip_warehouse`",
|
||||||
"uom",
|
"`tabJob Card Item`.`parent`",
|
||||||
"item_name",
|
"`tabJob Card Item`.`item_code`",
|
||||||
"source_warehouse",
|
"`tabJob Card Item`.`item_group`",
|
||||||
"required_qty",
|
"`tabJob Card Item`.`uom`",
|
||||||
"transferred_qty",
|
"`tabJob Card Item`.`item_name`",
|
||||||
|
"`tabJob Card Item`.`source_warehouse`",
|
||||||
|
"`tabJob Card Item`.`required_qty`",
|
||||||
|
"`tabJob Card Item`.`transferred_qty`",
|
||||||
],
|
],
|
||||||
filters={"parent": ["in", job_cards]},
|
filters={"name": job_card},
|
||||||
)
|
)
|
||||||
|
|
||||||
for row in data:
|
if not raw_materials:
|
||||||
raw_materials.setdefault(row.parent, []).append(row)
|
return []
|
||||||
|
|
||||||
|
for row in raw_materials:
|
||||||
|
warehouse = row.source_warehouse
|
||||||
|
if row.skip_material_transfer and row.backflush_from_wip_warehouse:
|
||||||
|
warehouse = row.wip_warehouse
|
||||||
|
|
||||||
|
row.stock_qty = (
|
||||||
|
frappe.db.get_value(
|
||||||
|
"Bin",
|
||||||
|
{
|
||||||
|
"item_code": row.item_code,
|
||||||
|
"warehouse": warehouse,
|
||||||
|
},
|
||||||
|
"actual_qty",
|
||||||
|
)
|
||||||
|
or 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
row.warehouse = warehouse
|
||||||
|
|
||||||
|
row.material_availability_status = 0
|
||||||
|
if row.skip_material_transfer and row.stock_qty >= row.required_qty:
|
||||||
|
row.material_availability_status = 1
|
||||||
|
elif row.transferred_qty >= row.required_qty:
|
||||||
|
row.material_availability_status = 1
|
||||||
|
|
||||||
return raw_materials
|
return raw_materials
|
||||||
|
|
||||||
@@ -392,20 +440,57 @@ def get_workstations(**kwargs):
|
|||||||
data = query.run(as_dict=True)
|
data = query.run(as_dict=True)
|
||||||
|
|
||||||
color_map = {
|
color_map = {
|
||||||
"Production": "var(--green-600)",
|
"Production": "green",
|
||||||
"Off": "var(--gray-600)",
|
"Off": "gray",
|
||||||
"Idle": "var(--gray-600)",
|
"Idle": "gray",
|
||||||
"Problem": "var(--red-600)",
|
"Problem": "red",
|
||||||
"Maintenance": "var(--yellow-600)",
|
"Maintenance": "yellow",
|
||||||
"Setup": "var(--blue-600)",
|
"Setup": "blue",
|
||||||
}
|
}
|
||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
d.workstation_name = get_link_to_form("Workstation", d.name)
|
d.workstation_name = get_link_to_form("Workstation", d.name)
|
||||||
d.status_image = d.on_status_image
|
d.status_image = d.on_status_image
|
||||||
d.background_color = color_map.get(d.status, "var(--red-600)")
|
d.color = color_map.get(d.status, "red")
|
||||||
d.workstation_link = get_url_to_form("Workstation", d.name)
|
d.workstation_link = get_url_to_form("Workstation", d.name)
|
||||||
if d.status != "Production":
|
if d.status != "Production":
|
||||||
d.status_image = d.off_status_image
|
d.status_image = d.off_status_image
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_job_card(job_card, method, **kwargs):
|
||||||
|
if isinstance(kwargs, dict):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
if kwargs.get("employees"):
|
||||||
|
kwargs.employees = frappe.parse_json(kwargs.employees)
|
||||||
|
|
||||||
|
if kwargs.qty and isinstance(kwargs.qty, str):
|
||||||
|
kwargs.qty = flt(kwargs.qty)
|
||||||
|
|
||||||
|
doc = frappe.get_doc("Job Card", job_card)
|
||||||
|
doc.run_method(method, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def validate_job_card(job_card, status):
|
||||||
|
job_card_details = frappe.db.get_value("Job Card", job_card, ["status", "for_quantity"], as_dict=1)
|
||||||
|
|
||||||
|
current_status = job_card_details.status
|
||||||
|
if current_status != status:
|
||||||
|
if status == "Open":
|
||||||
|
frappe.throw(
|
||||||
|
_("The job card {0} is in {1} state and you cannot start it again.").format(
|
||||||
|
job_card, current_status
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
frappe.throw(
|
||||||
|
_("The job card {0} is in {1} state and you cannot complete.").format(
|
||||||
|
job_card, current_status
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return job_card_details.for_quantity
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<style>
|
<style>
|
||||||
.job-card-link {
|
.job-card-link {
|
||||||
min-height: 100px;
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-head-job-card {
|
.section-head-job-card {
|
||||||
@@ -9,26 +9,31 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div style = "max-height: 400px; overflow-y: auto;">
|
<div>
|
||||||
|
|
||||||
|
<div class="form-dashboard-section qrcode-fields col-sm-12 section-body" style="padding-left: 0px;padding-right: 25px;"></div>
|
||||||
|
|
||||||
{% $.each(data, (idx, d) => { %}
|
{% $.each(data, (idx, d) => { %}
|
||||||
<div class="row form-dashboard-section job-card-link form-links border-gray-200" data-name="{{d.name}}">
|
<div class="row form-dashboard-section job-card-link form-links border-gray-200" data-name="{{d.name}}">
|
||||||
<div class="section-head section-head-job-card">
|
<div class="row form-section" style="width:100%;margin-top:10px">
|
||||||
{{ d.operation }} - {{ d.production_item }}
|
<div class="form-column col-sm-2">
|
||||||
<span class="ml-2 collapse-indicator-job mb-1" style="">
|
<div class="frappe-control bold" data-doctype="Job Card" data-name="{{d.name}}" title="{{__('Job Card ID')}}">
|
||||||
{{frappe.utils.icon("es-line-down", "sm", "mb-1")}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="row form-section" style="width:100%;margin-bottom:10px">
|
|
||||||
<div class="form-column col-sm-3">
|
|
||||||
<div class="frappe-control" title="{{__('Job Card')}}" style="text-decoration:underline">
|
|
||||||
{{ d.job_card_link }}
|
{{ d.job_card_link }}
|
||||||
</div>
|
</div>
|
||||||
<div class="frappe-control" title="{{__('Work Order')}}" style="text-decoration:underline">
|
|
||||||
{{ d.work_order_link }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-column col-sm-2">
|
<div class="form-column col-sm-2">
|
||||||
<div class="frappe-control timer" title="{{__('Timer')}}" style="text-align:center;font-size:14px;" data-job-card = {{escape(d.name)}}>
|
<div class="frappe-control" title="{{__('Item')}}">{{ d.finished_good || d.production_item }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-column col-sm-1">
|
||||||
|
<div class="frappe-control" title="{{__('Qty')}}">{{ d.for_quantity }} {{ d.fg_uom }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-column col-sm-2 ellipsis">
|
||||||
|
<div class="frappe-control" title="{{__('Operation')}}">
|
||||||
|
{{ d.operation_link }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-column col-sm-2">
|
||||||
|
<div class="frappe-control timer" title="{{__('Timer')}}" style="font-size:14px;" data-job-card = {{escape(d.name)}}>
|
||||||
<span class="hours">00</span>
|
<span class="hours">00</span>
|
||||||
<span class="colon">:</span>
|
<span class="colon">:</span>
|
||||||
<span class="minutes">00</span>
|
<span class="minutes">00</span>
|
||||||
@@ -36,7 +41,7 @@
|
|||||||
<span class="seconds">00</span>
|
<span class="seconds">00</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if(d.status === "Open") { %}
|
<!-- {% if(d.status === "Open") { %}
|
||||||
<div class="frappe-control" title="{{__('Expected Start Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
|
<div class="frappe-control" title="{{__('Expected Start Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
|
||||||
{{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }}
|
{{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -44,82 +49,60 @@
|
|||||||
<div class="frappe-control" title="{{__('Expected End Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
|
<div class="frappe-control" title="{{__('Expected End Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
|
||||||
{{ frappe.format(d.expected_end_date, { fieldtype: 'Datetime' }) }}
|
{{ frappe.format(d.expected_end_date, { fieldtype: 'Datetime' }) }}
|
||||||
</div>
|
</div>
|
||||||
{% } %}
|
{% } %} -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-column col-sm-2">
|
<div class="form-column col-sm-2">
|
||||||
<div class="frappe-control job-card-status" title="{{__('Status')}}" style="background:{{d.status_color}};text-align:center;border-radius:var(--border-radius-full)">
|
<div class="frappe-control indicator-pill no-indicator-dot whitespace-nowrap job-card-status {{d.status_color}}" title="{{__('Status')}}">
|
||||||
{{ d.status }}
|
{% if(d.status === "Open") { %}
|
||||||
|
{{__("Not Started")}}
|
||||||
|
{% } else { %}
|
||||||
|
{{ __(d.status) }}
|
||||||
|
{% } %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-column col-sm-2">
|
<div class="form-column col-sm-1">
|
||||||
<div class="frappe-control" title="{{__('Qty to Manufacture')}}">
|
<div class="menu-btn-group">
|
||||||
<div class="progress" title = "{{d.progress_title}}">
|
<button type="button" class="btn btn-default icon-btn" data-toggle="dropdown" aria-expanded="false" aria-label="{{ __("Menu") }}">
|
||||||
<div class="progress-bar progress-bar-success" style="width: {{d.progress_percent}}%">
|
<span>
|
||||||
</div>
|
<span class="menu-btn-group-label">
|
||||||
</div>
|
<svg class="icon icon-sm">
|
||||||
|
<use href="#icon-dot-vertical">
|
||||||
|
</use>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-right menu-actions" role="menu" data-job-card="{{d.name}}">
|
||||||
|
<li>
|
||||||
|
<a class="grey-link dropdown-item btn-start" href="#" onclick="return false;">
|
||||||
|
<span class="menu-item-label" data-label="Start">{{ __('Start') }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="grey-link dropdown-item btn-resume" href="#" onclick="return false;">
|
||||||
|
<span class="menu-item-label" data-label="Resume">{{ __('Resume') }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="grey-link dropdown-item btn-pause" href="#" onclick="return false;">
|
||||||
|
<span class="menu-item-label" data-label="Pause">{{ __('Pause') }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="grey-link dropdown-item btn-complete" href="#" onclick="return false;" data-qty="{{d.for_quantity}}">
|
||||||
|
<span class="menu-item-label" data-label="Complete">{{ __('Complete') }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="grey-link dropdown-item btn-transfer-materials" href="#" onclick="return false;">
|
||||||
|
<span class="menu-item-label" data-label="Transfer Materials">{{ __('Transfer Materials') }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="frappe-control" style="text-align: center; font-size: 10px;">
|
|
||||||
{{ d.for_quantity }} / {{ d.total_completed_qty }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-column col-sm-2 text-center">
|
|
||||||
<button style="width: 85px;" class="btn btn-default btn-start {% if(d.status !== "Open") { %} hide {% } %}" job-card="{{d.name}}"> {{__("Start")}} </button>
|
|
||||||
<button style="width: 85px;" class="btn btn-default btn-complete {% if(d.status === "Open") { %} hide {% } %}" job-card="{{d.name}}" pending-qty="{{d.for_quantity - d.transferred_qty}}"> {{__("Complete")}} </button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-body section-body-job-card form-section hide">
|
|
||||||
<hr>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-column col-sm-2">
|
|
||||||
{{ __("Raw Materials") }}
|
|
||||||
</div>
|
|
||||||
{% if(d.make_material_request) { %}
|
|
||||||
<div class="form-column col-sm-10 text-right">
|
|
||||||
<button class="btn btn-default btn-xs make-material-request" job-card="{{d.name}}">{{ __("Material Request") }}</button>
|
|
||||||
</div>
|
|
||||||
{% } %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if(d.raw_materials) { %}
|
|
||||||
<table class="table table-bordered table-condensed">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 5%" class="table-sr">Sr</th>
|
|
||||||
|
|
||||||
<th style="width: 15%">{{ __("Item") }}</th>
|
|
||||||
<th style="width: 15%">{{ __("Warehouse") }}</th>
|
|
||||||
<th style="width: 10%">{{__("UOM")}}</th>
|
|
||||||
<th style="width: 15%">{{__("Item Group")}}</th>
|
|
||||||
<th style="width: 20%" >{{__("Required Qty")}}</th>
|
|
||||||
<th style="width: 20%" >{{__("Transferred Qty")}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
{% $.each(d.raw_materials, (row_index, child_row) => { %}
|
|
||||||
<tr>
|
|
||||||
<td class="table-sr">{{ row_index+1 }}</td>
|
|
||||||
{% if(child_row.item_code === child_row.item_name) { %}
|
|
||||||
<td>{{ child_row.item_code }}</td>
|
|
||||||
{% } else { %}
|
|
||||||
<td>{{ child_row.item_code }}: {{child_row.item_name}}</td>
|
|
||||||
{% } %}
|
|
||||||
<td>{{ child_row.source_warehouse }}</td>
|
|
||||||
<td>{{ child_row.uom }}</td>
|
|
||||||
<td>{{ child_row.item_group }}</td>
|
|
||||||
<td>{{ child_row.required_qty }}</td>
|
|
||||||
<td>{{ child_row.transferred_qty }}</td>
|
|
||||||
</tr>
|
|
||||||
{% }); %}
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
{% } %}
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% }); %}
|
{% }); %}
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +102,12 @@ def get_columns() -> Columns:
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"width": "150",
|
"width": "150",
|
||||||
},
|
},
|
||||||
{"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"},
|
{
|
||||||
|
"label": _("Finished Goods Value"),
|
||||||
|
"fieldname": "total_fg_value",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"width": "150",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": _("Raw Material Value"),
|
"label": _("Raw Material Value"),
|
||||||
"fieldname": "total_rm_value",
|
"fieldname": "total_rm_value",
|
||||||
|
|||||||
@@ -367,3 +367,6 @@ erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
|||||||
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||||
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||||
|
erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations
|
||||||
|
erpnext.patches.v15_0.add_default_operations
|
||||||
|
erpnext.patches.v15_0.enable_old_serial_batch_fields
|
||||||
|
|||||||
5
erpnext/patches/v15_0/add_default_operations.py
Normal file
5
erpnext/patches/v15_0/add_default_operations.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from erpnext.setup.install import make_default_operations
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
make_default_operations()
|
||||||
@@ -43,7 +43,7 @@ def get_details_of_draft_or_submitted_depreciable_assets():
|
|||||||
asset.name,
|
asset.name,
|
||||||
asset.opening_accumulated_depreciation,
|
asset.opening_accumulated_depreciation,
|
||||||
asset.gross_purchase_amount,
|
asset.gross_purchase_amount,
|
||||||
asset.number_of_depreciations_booked,
|
asset.opening_number_of_booked_depreciations,
|
||||||
asset.docstatus,
|
asset.docstatus,
|
||||||
)
|
)
|
||||||
.where(asset.calculate_depreciation == 1)
|
.where(asset.calculate_depreciation == 1)
|
||||||
|
|||||||
7
erpnext/patches/v15_0/enable_old_serial_batch_fields.py
Normal file
7
erpnext/patches/v15_0/enable_old_serial_batch_fields.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
sabb = frappe.get_all("Serial and Batch Bundle", filters={"docstatus": ("<", 2)}, limit=1)
|
||||||
|
if not sabb:
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.model.utils.rename_field import rename_field
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.db.has_column("Asset", "number_of_depreciations_booked"):
|
||||||
|
rename_field("Asset", "number_of_depreciations_booked", "opening_number_of_booked_depreciations")
|
||||||
@@ -9,12 +9,12 @@ def execute():
|
|||||||
ON `tabAsset Depreciation Schedule`.`asset`=`tabAsset`.`name`
|
ON `tabAsset Depreciation Schedule`.`asset`=`tabAsset`.`name`
|
||||||
SET
|
SET
|
||||||
`tabAsset Depreciation Schedule`.`gross_purchase_amount`=`tabAsset`.`gross_purchase_amount`,
|
`tabAsset Depreciation Schedule`.`gross_purchase_amount`=`tabAsset`.`gross_purchase_amount`,
|
||||||
`tabAsset Depreciation Schedule`.`number_of_depreciations_booked`=`tabAsset`.`number_of_depreciations_booked`
|
`tabAsset Depreciation Schedule`.`opening_number_of_booked_depreciations`=`tabAsset`.`opening_number_of_booked_depreciations`
|
||||||
WHERE
|
WHERE
|
||||||
(
|
(
|
||||||
`tabAsset Depreciation Schedule`.`gross_purchase_amount`<>`tabAsset`.`gross_purchase_amount`
|
`tabAsset Depreciation Schedule`.`gross_purchase_amount`<>`tabAsset`.`gross_purchase_amount`
|
||||||
OR
|
OR
|
||||||
`tabAsset Depreciation Schedule`.`number_of_depreciations_booked`<>`tabAsset`.`number_of_depreciations_booked`
|
`tabAsset Depreciation Schedule`.`opening_number_of_booked_depreciations`<>`tabAsset`.`opening_number_of_booked_depreciations`
|
||||||
)
|
)
|
||||||
AND `tabAsset Depreciation Schedule`.`docstatus`<2"""
|
AND `tabAsset Depreciation Schedule`.`docstatus`<2"""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ frappe.ui.form.on("Task", {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
frm.set_query("task", "depends_on", function () {
|
frm.set_query("task", "depends_on", function () {
|
||||||
let filters = {
|
let filters = {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"collapsible_depends_on": "eval:doc.__islocal",
|
"collapsible_depends_on": "exp_start_date",
|
||||||
"fieldname": "sb_timeline",
|
"fieldname": "sb_timeline",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Timeline"
|
"label": "Timeline"
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
{
|
{
|
||||||
"bold": 1,
|
"bold": 1,
|
||||||
"fieldname": "exp_start_date",
|
"fieldname": "exp_start_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Datetime",
|
||||||
"label": "Expected Start Date",
|
"label": "Expected Start Date",
|
||||||
"oldfieldname": "exp_start_date",
|
"oldfieldname": "exp_start_date",
|
||||||
"oldfieldtype": "Date"
|
"oldfieldtype": "Date"
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
{
|
{
|
||||||
"bold": 1,
|
"bold": 1,
|
||||||
"fieldname": "exp_end_date",
|
"fieldname": "exp_end_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Datetime",
|
||||||
"label": "Expected End Date",
|
"label": "Expected End Date",
|
||||||
"oldfieldname": "exp_end_date",
|
"oldfieldname": "exp_end_date",
|
||||||
"oldfieldtype": "Date",
|
"oldfieldtype": "Date",
|
||||||
@@ -399,7 +399,7 @@
|
|||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"max_attachments": 5,
|
"max_attachments": 5,
|
||||||
"modified": "2024-03-27 13:10:51.476856",
|
"modified": "2024-05-24 12:36:12.214577",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Task",
|
"name": "Task",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import frappe
|
|||||||
from frappe import _, throw
|
from frappe import _, throw
|
||||||
from frappe.desk.form.assign_to import clear, close_all_assignments
|
from frappe.desk.form.assign_to import clear, close_all_assignments
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import add_days, cstr, date_diff, flt, get_link_to_form, getdate, today
|
from frappe.utils import add_days, add_to_date, cstr, date_diff, flt, get_link_to_form, getdate, today
|
||||||
from frappe.utils.data import format_date
|
from frappe.utils.data import format_date
|
||||||
from frappe.utils.nestedset import NestedSet
|
from frappe.utils.nestedset import NestedSet
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ class Task(NestedSet):
|
|||||||
depends_on_tasks: DF.Code | None
|
depends_on_tasks: DF.Code | None
|
||||||
description: DF.TextEditor | None
|
description: DF.TextEditor | None
|
||||||
duration: DF.Int
|
duration: DF.Int
|
||||||
exp_end_date: DF.Date | None
|
exp_end_date: DF.Datetime | None
|
||||||
exp_start_date: DF.Date | None
|
exp_start_date: DF.Datetime | None
|
||||||
expected_time: DF.Float
|
expected_time: DF.Float
|
||||||
is_group: DF.Check
|
is_group: DF.Check
|
||||||
is_milestone: DF.Check
|
is_milestone: DF.Check
|
||||||
@@ -83,6 +83,7 @@ class Task(NestedSet):
|
|||||||
self.update_depends_on()
|
self.update_depends_on()
|
||||||
self.validate_dependencies_for_template_task()
|
self.validate_dependencies_for_template_task()
|
||||||
self.validate_completed_on()
|
self.validate_completed_on()
|
||||||
|
self.set_default_end_date_if_missing()
|
||||||
|
|
||||||
def validate_dates(self):
|
def validate_dates(self):
|
||||||
self.validate_from_to_dates("exp_start_date", "exp_end_date")
|
self.validate_from_to_dates("exp_start_date", "exp_end_date")
|
||||||
@@ -90,6 +91,10 @@ class Task(NestedSet):
|
|||||||
self.validate_parent_expected_end_date()
|
self.validate_parent_expected_end_date()
|
||||||
self.validate_parent_project_dates()
|
self.validate_parent_project_dates()
|
||||||
|
|
||||||
|
def set_default_end_date_if_missing(self):
|
||||||
|
if self.exp_start_date and self.expected_time:
|
||||||
|
self.exp_end_date = add_to_date(self.exp_start_date, hours=self.expected_time)
|
||||||
|
|
||||||
def validate_parent_expected_end_date(self):
|
def validate_parent_expected_end_date(self):
|
||||||
if not self.parent_task or not self.exp_end_date:
|
if not self.parent_task or not self.exp_end_date:
|
||||||
return
|
return
|
||||||
@@ -199,8 +204,6 @@ class Task(NestedSet):
|
|||||||
self.name,
|
self.name,
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)[0]
|
)[0]
|
||||||
if self.status == "Open":
|
|
||||||
self.status = "Working"
|
|
||||||
self.total_costing_amount = tl.total_costing_amount
|
self.total_costing_amount = tl.total_costing_amount
|
||||||
self.total_billing_amount = tl.total_billing_amount
|
self.total_billing_amount = tl.total_billing_amount
|
||||||
self.actual_time = tl.time
|
self.actual_time = tl.time
|
||||||
@@ -250,7 +253,7 @@ class Task(NestedSet):
|
|||||||
if (
|
if (
|
||||||
task.exp_start_date
|
task.exp_start_date
|
||||||
and task.exp_end_date
|
and task.exp_end_date
|
||||||
and task.exp_start_date < getdate(end_date)
|
and task.exp_start_date < end_date
|
||||||
and task.status == "Open"
|
and task.status == "Open"
|
||||||
):
|
):
|
||||||
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
|
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
|
||||||
@@ -288,7 +291,7 @@ class Task(NestedSet):
|
|||||||
if self.status not in ("Cancelled", "Completed") and self.exp_end_date:
|
if self.status not in ("Cancelled", "Completed") and self.exp_end_date:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
if self.exp_end_date < datetime.now().date():
|
if self.exp_end_date < datetime.now():
|
||||||
self.db_set("status", "Overdue", update_modified=False)
|
self.db_set("status", "Overdue", update_modified=False)
|
||||||
self.update_project()
|
self.update_project()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
@@ -44,17 +43,21 @@ class TestTask(unittest.TestCase):
|
|||||||
task1.save()
|
task1.save()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate(add_days(nowdate(), 21))
|
getdate(frappe.db.get_value("Task", task2.name, "exp_start_date")),
|
||||||
)
|
getdate(add_days(nowdate(), 21)),
|
||||||
self.assertEqual(
|
|
||||||
frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate(add_days(nowdate(), 25))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate(add_days(nowdate(), 26))
|
getdate(frappe.db.get_value("Task", task2.name, "exp_end_date")), getdate(add_days(nowdate(), 25))
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate(add_days(nowdate(), 30))
|
getdate(frappe.db.get_value("Task", task3.name, "exp_start_date")),
|
||||||
|
getdate(add_days(nowdate(), 26)),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
getdate(frappe.db.get_value("Task", task3.name, "exp_end_date")), getdate(add_days(nowdate(), 30))
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_close_assignment(self):
|
def test_close_assignment(self):
|
||||||
|
|||||||
@@ -126,6 +126,12 @@ class Timesheet(Document):
|
|||||||
if data.task and data.task not in tasks:
|
if data.task and data.task not in tasks:
|
||||||
task = frappe.get_doc("Task", data.task)
|
task = frappe.get_doc("Task", data.task)
|
||||||
task.update_time_and_costing()
|
task.update_time_and_costing()
|
||||||
|
time_logs_completed = all(tl.completed for tl in self.time_logs if tl.task == task.name)
|
||||||
|
|
||||||
|
if time_logs_completed:
|
||||||
|
task.status = "Completed"
|
||||||
|
else:
|
||||||
|
task.status = "Working"
|
||||||
task.save()
|
task.save()
|
||||||
tasks.append(data.task)
|
tasks.append(data.task)
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ class BOMConfigurator {
|
|||||||
frm: this.frm,
|
frm: this.frm,
|
||||||
add_item: this.add_item,
|
add_item: this.add_item,
|
||||||
add_sub_assembly: this.add_sub_assembly,
|
add_sub_assembly: this.add_sub_assembly,
|
||||||
|
set_query_for_workstation: this.set_query_for_workstation,
|
||||||
get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields,
|
get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields,
|
||||||
convert_to_sub_assembly: this.convert_to_sub_assembly,
|
convert_to_sub_assembly: this.convert_to_sub_assembly,
|
||||||
delete_node: this.delete_node,
|
delete_node: this.delete_node,
|
||||||
edit_qty: this.edit_qty,
|
edit_bom: this.edit_bom,
|
||||||
load_tree: this.load_tree,
|
load_tree: this.load_tree,
|
||||||
set_default_qty: this.set_default_qty,
|
set_default_qty: this.set_default_qty,
|
||||||
};
|
};
|
||||||
@@ -107,15 +108,15 @@ class BOMConfigurator {
|
|||||||
this.frm?.doc.docstatus === 0
|
this.frm?.doc.docstatus === 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: `${frappe.utils.icon("edit", "sm")} ${__("Qty")}`,
|
label: __(frappe.utils.icon("edit", "sm") + " BOM"),
|
||||||
click: function (node) {
|
click: function (node) {
|
||||||
let view = frappe.views.trees["BOM Configurator"];
|
let view = frappe.views.trees["BOM Configurator"];
|
||||||
view.events.edit_qty(node, view);
|
view.events.edit_bom(node, view);
|
||||||
},
|
},
|
||||||
btnClass: "hidden-xs",
|
btnClass: "hidden-xs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `${frappe.utils.icon("add", "sm")} ${__("Raw Material")}`,
|
label: __(frappe.utils.icon("add", "sm") + " Raw Material"),
|
||||||
click: function (node) {
|
click: function (node) {
|
||||||
let view = frappe.views.trees["BOM Configurator"];
|
let view = frappe.views.trees["BOM Configurator"];
|
||||||
view.events.add_item(node, view);
|
view.events.add_item(node, view);
|
||||||
@@ -126,7 +127,7 @@ class BOMConfigurator {
|
|||||||
btnClass: "hidden-xs",
|
btnClass: "hidden-xs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `${frappe.utils.icon("add", "sm")} ${__("Sub Assembly")}`,
|
label: __(frappe.utils.icon("add", "sm") + " Sub Assembly"),
|
||||||
click: function (node) {
|
click: function (node) {
|
||||||
let view = frappe.views.trees["BOM Configurator"];
|
let view = frappe.views.trees["BOM Configurator"];
|
||||||
view.events.add_sub_assembly(node, view);
|
view.events.add_sub_assembly(node, view);
|
||||||
@@ -156,7 +157,7 @@ class BOMConfigurator {
|
|||||||
btnClass: "hidden-xs expand-all-btn",
|
btnClass: "hidden-xs expand-all-btn",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `${frappe.utils.icon("move", "sm")} ${__("Sub Assembly")}`,
|
label: __(frappe.utils.icon("move", "sm") + " Sub Assembly"),
|
||||||
click: function (node) {
|
click: function (node) {
|
||||||
let view = frappe.views.trees["BOM Configurator"];
|
let view = frappe.views.trees["BOM Configurator"];
|
||||||
view.events.convert_to_sub_assembly(node, view);
|
view.events.convert_to_sub_assembly(node, view);
|
||||||
@@ -167,7 +168,7 @@ class BOMConfigurator {
|
|||||||
btnClass: "hidden-xs",
|
btnClass: "hidden-xs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `${frappe.utils.icon("delete", "sm")} ${__("Item")}`,
|
label: __(frappe.utils.icon("delete", "sm") + " Item"),
|
||||||
click: function (node) {
|
click: function (node) {
|
||||||
let view = frappe.views.trees["BOM Configurator"];
|
let view = frappe.views.trees["BOM Configurator"];
|
||||||
view.events.delete_node(node, view);
|
view.events.delete_node(node, view);
|
||||||
@@ -232,18 +233,38 @@ class BOMConfigurator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_query_for_workstation(dialog) {
|
||||||
|
let workstation = dialog.fields.filter((field) => field.fieldname === "workstation");
|
||||||
|
if (workstation.length) {
|
||||||
|
workstation[0].get_query = function () {
|
||||||
|
let workstation_type = dialog.get_value("workstation_type");
|
||||||
|
|
||||||
|
if (workstation_type) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
workstation_type: dialog.get_value("workstation_type"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
add_sub_assembly(node, view) {
|
add_sub_assembly(node, view) {
|
||||||
let dialog = new frappe.ui.Dialog({
|
let dialog = new frappe.ui.Dialog({
|
||||||
fields: view.events.get_sub_assembly_modal_fields(),
|
fields: view.events.get_sub_assembly_modal_fields(view, node.is_root),
|
||||||
title: __("Add Sub Assembly"),
|
title: __("Add Sub Assembly"),
|
||||||
});
|
});
|
||||||
|
view.events.set_query_for_workstation(dialog);
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
view.events.set_default_qty(dialog);
|
|
||||||
|
|
||||||
dialog.set_primary_action(__("Add"), () => {
|
dialog.set_primary_action(__("Add"), () => {
|
||||||
let bom_item = dialog.get_values();
|
let bom_item = dialog.get_values();
|
||||||
|
|
||||||
|
if (dialog.operation && !dialog.workstation_type && !dialog.workstation) {
|
||||||
|
frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
|
||||||
|
}
|
||||||
|
|
||||||
if (!node.data?.parent_id) {
|
if (!node.data?.parent_id) {
|
||||||
node.data.parent_id = this.frm.doc.name;
|
node.data.parent_id = this.frm.doc.name;
|
||||||
}
|
}
|
||||||
@@ -255,6 +276,9 @@ class BOMConfigurator {
|
|||||||
fg_item: node.data.value,
|
fg_item: node.data.value,
|
||||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||||
bom_item: bom_item,
|
bom_item: bom_item,
|
||||||
|
operation: node.data.operation,
|
||||||
|
workstation_type: node.data.workstation_type,
|
||||||
|
operation_time: node.data.operation_time,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
view.events.load_tree(r, node);
|
view.events.load_tree(r, node);
|
||||||
@@ -265,8 +289,8 @@ class BOMConfigurator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get_sub_assembly_modal_fields(read_only = false) {
|
get_sub_assembly_modal_fields(view, is_root = false, read_only = false, show_operations_fields = false) {
|
||||||
return [
|
let fields = [
|
||||||
{
|
{
|
||||||
label: __("Sub Assembly Item"),
|
label: __("Sub Assembly Item"),
|
||||||
fieldname: "item_code",
|
fieldname: "item_code",
|
||||||
@@ -284,37 +308,150 @@ class BOMConfigurator {
|
|||||||
reqd: 1,
|
reqd: 1,
|
||||||
read_only: read_only,
|
read_only: read_only,
|
||||||
},
|
},
|
||||||
{ fieldtype: "Section Break" },
|
|
||||||
{
|
|
||||||
label: __("Raw Materials"),
|
|
||||||
fieldname: "items",
|
|
||||||
fieldtype: "Table",
|
|
||||||
reqd: 1,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: __("Item"),
|
|
||||||
fieldname: "item_code",
|
|
||||||
fieldtype: "Link",
|
|
||||||
options: "Item",
|
|
||||||
reqd: 1,
|
|
||||||
in_list_view: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: __("Qty"),
|
|
||||||
fieldname: "qty",
|
|
||||||
default: 1.0,
|
|
||||||
fieldtype: "Float",
|
|
||||||
reqd: 1,
|
|
||||||
in_list_view: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (this.frm.doc.track_operations && (is_root || show_operations_fields)) {
|
||||||
|
fields.push(
|
||||||
|
...[
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
|
{
|
||||||
|
label: __("Operation"),
|
||||||
|
fieldname: "operation",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Operation",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Operation Time"),
|
||||||
|
fieldname: "operation_time",
|
||||||
|
fieldtype: "Int",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Is Subcontracted"),
|
||||||
|
fieldname: "is_subcontracted",
|
||||||
|
fieldtype: "Check",
|
||||||
|
},
|
||||||
|
{ fieldtype: "Column Break" },
|
||||||
|
{
|
||||||
|
label: __("Workstation Type"),
|
||||||
|
fieldname: "workstation_type",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Workstation Type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Workstation"),
|
||||||
|
fieldname: "workstation",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Workstation",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.frm.doc.track_semi_finished_goods) {
|
||||||
|
fields.push(
|
||||||
|
...[
|
||||||
|
{ label: __("Default Warehouse"), fieldtype: "Section Break", collapsible: 1 },
|
||||||
|
{
|
||||||
|
label: __("Skip Material Transfer"),
|
||||||
|
fieldname: "skip_material_transfer",
|
||||||
|
fieldtype: "Check",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Backflush Materials From WIP"),
|
||||||
|
fieldname: "backflush_from_wip_warehouse",
|
||||||
|
fieldtype: "Check",
|
||||||
|
depends_on: "eval:doc.skip_material_transfer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Source Warehouse"),
|
||||||
|
fieldname: "source_warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Warehouse",
|
||||||
|
depends_on: "eval:!doc.backflush_from_wip_warehouse",
|
||||||
|
get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: view.events.frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ fieldtype: "Column Break" },
|
||||||
|
{
|
||||||
|
label: __("Work In Progress Warehouse"),
|
||||||
|
fieldname: "wip_warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Warehouse",
|
||||||
|
depends_on:
|
||||||
|
"eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
|
||||||
|
get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: view.events.frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Finished Good Warehouse"),
|
||||||
|
fieldname: "fg_warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Warehouse",
|
||||||
|
get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: view.events.frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.push(
|
||||||
|
...[
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
|
{
|
||||||
|
label: __("Raw Materials"),
|
||||||
|
fieldname: "items",
|
||||||
|
fieldtype: "Table",
|
||||||
|
reqd: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: __("Item"),
|
||||||
|
fieldname: "item_code",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item",
|
||||||
|
reqd: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
change() {
|
||||||
|
let doc = this.doc;
|
||||||
|
doc.qty = 1.0;
|
||||||
|
this.grid.set_value("qty", 1.0, doc);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Qty"),
|
||||||
|
fieldname: "qty",
|
||||||
|
default: 1.0,
|
||||||
|
fieldtype: "Float",
|
||||||
|
reqd: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
convert_to_sub_assembly(node, view) {
|
convert_to_sub_assembly(node, view) {
|
||||||
let dialog = new frappe.ui.Dialog({
|
let dialog = new frappe.ui.Dialog({
|
||||||
fields: view.events.get_sub_assembly_modal_fields(true),
|
fields: view.events.get_sub_assembly_modal_fields(view, node.is_root, true, true),
|
||||||
title: __("Add Sub Assembly"),
|
title: __("Add Sub Assembly"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -324,11 +461,13 @@ class BOMConfigurator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
view.events.set_default_qty(dialog);
|
|
||||||
|
|
||||||
dialog.set_primary_action(__("Add"), () => {
|
dialog.set_primary_action(__("Add"), () => {
|
||||||
let bom_item = dialog.get_values();
|
let bom_item = dialog.get_values();
|
||||||
|
|
||||||
|
if (dialog.operation && !dialog.workstation_type && !dialog.workstation) {
|
||||||
|
frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
|
||||||
|
}
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
||||||
args: {
|
args: {
|
||||||
@@ -337,10 +476,14 @@ class BOMConfigurator {
|
|||||||
bom_item: bom_item,
|
bom_item: bom_item,
|
||||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||||
convert_to_sub_assembly: true,
|
convert_to_sub_assembly: true,
|
||||||
|
operation: node.data.operation,
|
||||||
|
workstation_type: node.data.workstation_type,
|
||||||
|
operation_time: node.data.operation_time,
|
||||||
|
workstation: node.data.workstation,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
node.expandable = true;
|
node.expandable = true;
|
||||||
view.events.load_tree(r, node);
|
view.events.load_tree(r, node.parent_node);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -376,24 +519,147 @@ class BOMConfigurator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
edit_qty(node, view) {
|
edit_bom(node, view) {
|
||||||
|
let me = this;
|
||||||
let qty = node.data.qty || this.frm.doc.qty;
|
let qty = node.data.qty || this.frm.doc.qty;
|
||||||
frappe.prompt(
|
let fields = [{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }];
|
||||||
[{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }],
|
|
||||||
|
if (node.expandable && this.frm.doc.track_operations) {
|
||||||
|
let data = node.data.operation ? node.data : this.frm.doc;
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
...fields,
|
||||||
|
...[
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
|
{
|
||||||
|
label: __("Operation"),
|
||||||
|
fieldname: "operation",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Operation",
|
||||||
|
default: data.operation,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Operation Time"),
|
||||||
|
fieldname: "operation_time",
|
||||||
|
fieldtype: "Float",
|
||||||
|
default: data.operation_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Is Subcontracted"),
|
||||||
|
fieldname: "is_subcontracted",
|
||||||
|
fieldtype: "Check",
|
||||||
|
hidden: node?.is_root || 0,
|
||||||
|
default: data.is_subcontracted,
|
||||||
|
},
|
||||||
|
{ fieldtype: "Column Break" },
|
||||||
|
{
|
||||||
|
label: __("Workstation Type"),
|
||||||
|
fieldname: "workstation_type",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Workstation Type",
|
||||||
|
default: data.workstation_type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Workstation"),
|
||||||
|
fieldname: "workstation",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Workstation",
|
||||||
|
default: data.workstation,
|
||||||
|
get_query() {
|
||||||
|
let dialog = me.frm.edit_bom_dialog;
|
||||||
|
let workstation_type = dialog.get_value("workstation_type");
|
||||||
|
|
||||||
|
if (workstation_type) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
workstation_type: dialog.get_value("workstation_type"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
|
{
|
||||||
|
label: __("Skip Material Transfer"),
|
||||||
|
fieldname: "skip_material_transfer",
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: data.skip_material_transfer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Backflush Materials From WIP"),
|
||||||
|
fieldname: "backflush_from_wip_warehouse",
|
||||||
|
fieldtype: "Check",
|
||||||
|
depends_on: "eval:doc.skip_material_transfer",
|
||||||
|
default: data.backflush_from_wip_warehouse,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Source Warehouse"),
|
||||||
|
fieldname: "source_warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Warehouse",
|
||||||
|
default: data.source_warehouse,
|
||||||
|
depends_on: "eval:!doc.backflush_from_wip_warehouse",
|
||||||
|
get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: me.frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ fieldtype: "Column Break" },
|
||||||
|
{
|
||||||
|
label: __("Work In Progress Warehouse"),
|
||||||
|
fieldname: "wip_warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Warehouse",
|
||||||
|
default: data.wip_warehouse,
|
||||||
|
depends_on: "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
|
||||||
|
get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: me.frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Finished Good Warehouse"),
|
||||||
|
fieldname: "fg_warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Warehouse",
|
||||||
|
default: data.fg_warehouse,
|
||||||
|
get_query() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: me.frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frm.edit_bom_dialog = frappe.prompt(
|
||||||
|
fields,
|
||||||
(data) => {
|
(data) => {
|
||||||
let doctype = node.data.doctype || this.frm.doc.doctype;
|
let doctype = node.data.doctype || this.frm.doc.doctype;
|
||||||
let docname = node.data.name || this.frm.doc.name;
|
let docname = node.data.name || this.frm.doc.name;
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty",
|
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_bom_creator",
|
||||||
args: {
|
args: {
|
||||||
doctype: doctype,
|
doctype: doctype,
|
||||||
docname: docname,
|
docname: docname,
|
||||||
qty: data.qty,
|
data: data,
|
||||||
parent: node.data.parent_id,
|
parent: node.data.parent_id || this.frm.doc.name,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
node.data.qty = data.qty;
|
for (let key in data) {
|
||||||
|
node.data[key] = data[key];
|
||||||
|
}
|
||||||
|
|
||||||
let uom = node.data.uom || this.frm.doc.uom;
|
let uom = node.data.uom || this.frm.doc.uom;
|
||||||
$(node.parent.get(0))
|
$(node.parent.get(0))
|
||||||
.find(`[data-bom-qty-docname='${docname}']`)
|
.find(`[data-bom-qty-docname='${docname}']`)
|
||||||
@@ -402,7 +668,7 @@ class BOMConfigurator {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
__("Edit Qty"),
|
__("Edit BOM"),
|
||||||
__("Update")
|
__("Update")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.refresh_field("taxes");
|
this.frm.refresh_fields();
|
||||||
}
|
}
|
||||||
|
|
||||||
calculate_discount_amount() {
|
calculate_discount_amount() {
|
||||||
@@ -853,7 +853,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.refresh_field("taxes");
|
this.frm.refresh_fields();
|
||||||
}
|
}
|
||||||
|
|
||||||
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
||||||
|
|||||||
@@ -1256,8 +1256,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
qty(doc, cdt, cdn) {
|
qty(doc, cdt, cdn) {
|
||||||
if (!this.frm.doc.__onload?.load_after_mapping) {
|
let item = frappe.get_doc(cdt, cdn);
|
||||||
let item = frappe.get_doc(cdt, cdn);
|
if (!this.is_a_mapped_document(item)) {
|
||||||
// item.pricing_rules = ''
|
// item.pricing_rules = ''
|
||||||
frappe.run_serially([
|
frappe.run_serially([
|
||||||
() => this.remove_pricing_rule_for_item(item),
|
() => this.remove_pricing_rule_for_item(item),
|
||||||
@@ -2308,6 +2308,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
|
|
||||||
if (doc.is_return) {
|
if (doc.is_return) {
|
||||||
filters["is_return"] = 1;
|
filters["is_return"] = 1;
|
||||||
|
if (["Sales Invoice", "Delivery Note"].includes(doc.doctype)) {
|
||||||
|
filters["is_inward"] = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.warehouse) filters["warehouse"] = item.warehouse;
|
if (item.warehouse) filters["warehouse"] = item.warehouse;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{% $.each(workstations, (idx, row) => { %}
|
{% $.each(workstations, (idx, row) => { %}
|
||||||
<div class="workstation-wrapper">
|
<div class="workstation-wrapper">
|
||||||
|
<div class="workstation-status text-right">
|
||||||
|
<span class="indicator-pill no-indicator-dot whitespace-nowrap {{row.color}}" style="margin: 3px 4px 0px 0px;"><span style="font-size:13px">{{row.status}}</span></span>
|
||||||
|
</div>
|
||||||
<div class="workstation-image">
|
<div class="workstation-image">
|
||||||
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
|
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
|
||||||
<a class="workstation-image-link" href="{{row.workstation_link}}">
|
<a class="workstation-image-link" href="{{row.workstation_link}}">
|
||||||
@@ -11,9 +14,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="workstation-card text-center">
|
<div class="workstation-card" style="display: grid;">
|
||||||
<p style="background-color:{{row.background_color}};color:#fff">{{row.status}}</p>
|
<span class="ellipsis" title="{{row.name}}">
|
||||||
<div>{{row.workstation_name}}</div>
|
{{row.workstation_name}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% }); %}
|
{% }); %}
|
||||||
@@ -613,7 +613,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_data() {
|
render_data() {
|
||||||
if (this.bundle) {
|
if (this.bundle || this.frm.doc.is_return) {
|
||||||
frappe
|
frappe
|
||||||
.call({
|
.call({
|
||||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers",
|
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers",
|
||||||
@@ -621,6 +621,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
item_code: this.item.item_code,
|
item_code: this.item.item_code,
|
||||||
name: this.bundle,
|
name: this.bundle,
|
||||||
voucher_no: !this.frm.is_new() ? this.item.parent : "",
|
voucher_no: !this.frm.is_new() ? this.item.parent : "",
|
||||||
|
child_row: this.frm.doc.is_return ? this.item : "",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
|
|||||||
@@ -511,6 +511,10 @@ body[data-route="pos"] {
|
|||||||
padding-bottom: 25px;
|
padding-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workstation-image-cls {
|
||||||
|
height: 9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.plant-floor-filter {
|
.plant-floor-filter {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -519,8 +523,8 @@ body[data-route="pos"] {
|
|||||||
|
|
||||||
.plant-floor-container {
|
.plant-floor-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
gap: var(--margin-xl);
|
gap: var(--margin-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 620px) {
|
@media screen and (max-width: 620px) {
|
||||||
@@ -536,7 +540,7 @@ body[data-route="pos"] {
|
|||||||
.plant-floor-container .workstation-image-link {
|
.plant-floor-container .workstation-image-link {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
margin: var(--margin-sm);
|
margin: var(--margin-xs);
|
||||||
min-height: 9rem;
|
min-height: 9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user