Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into gstr_1_cdnr_unregistered_json

This commit is contained in:
Deepesh Garg
2021-07-06 11:27:18 +05:30
216 changed files with 7686 additions and 3436 deletions

View File

@@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
if address and frappe.db.get_value('Dynamic Link', if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}): {'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address]) filters.append(["Address", "name", "=", address])
if not address:
filters.append(["Address", "is_shipping_address", "=", 1])
address = frappe.get_all("Address", filters=filters, fields=fields) or {} address = frappe.get_all("Address", filters=filters, fields=fields) or {}

View File

@@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
amount, base_amount = calculate_amount(doc, item, last_gl_entry, amount, base_amount = calculate_amount(doc, item, last_gl_entry,
total_days, total_booking_days, account_currency) total_days, total_booking_days, account_currency)
if not amount:
return
if via_journal_entry: if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
@@ -298,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
start_date = add_months(today(), -1) start_date = add_months(today(), -1)
end_date = add_days(today(), -1) end_date = add_days(today(), -1)
for record_type in ('Income', 'Expense'): companies = frappe.get_all('Company')
doc = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=posting_date,
start_date=start_date,
end_date=end_date,
type=record_type
))
doc.insert() for company in companies:
doc.submit() for record_type in ('Income', 'Expense'):
doc = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
company=company.name,
posting_date=posting_date,
start_date=start_date,
end_date=end_date,
type=record_type
))
doc.insert()
doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against, def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):

View File

@@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self): def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
'Cost Center', 'Accounting Dimension Detail', 'Company') : 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg) frappe.throw(msg)

View File

@@ -260,7 +260,7 @@
"description": "If enabled, ledger entries will be posted for change amount in POS transactions", "description": "If enabled, ledger entries will be posted for change amount in POS transactions",
"fieldname": "post_change_gl_entries", "fieldname": "post_change_gl_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Change Ledger Entries for Change Amount" "label": "Create Ledger Entries for Change Amount"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -268,7 +268,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-06-16 13:14:45.739107", "modified": "2021-06-17 20:26:03.721202",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -51,7 +51,7 @@ class BankStatementImport(DataImport):
self.import_file, self.google_sheets_url self.import_file, self.google_sheets_url
) )
if 'Bank Account' not in json.dumps(preview): if 'Bank Account' not in json.dumps(preview['columns']):
frappe.throw(_("Please add the Bank Account column")) frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info from frappe.core.page.background_jobs.background_jobs import get_info

View File

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

View File

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

View File

@@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}); });
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() { onload: function() {
this._super(); this._super();
@@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
frm: frm, frm: frm,
freeze_message: __("Creating Purchase Receipt ...") freeze_message: __("Creating Purchase Receipt ...")
}) })
} },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
}) })

View File

@@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController):
# 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.update_stock_ledger() self.update_stock_ledger()
self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
@@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()

View File

@@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(actual_qty_0, get_qty_after_transaction()) self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self): def test_subcontracting_via_purchase_invoice(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
update_backflush_based_on('BOM')
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC", make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100) qty=100, basic_rate=100)
@@ -964,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase):
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate()) update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
# Create Purchase Order with TDS applied # Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000) po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
po.apply_tds = 1 po.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save() po.save()
@@ -1000,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase):
# Create Purchase Invoice against Purchase Order # Create Purchase Invoice against Purchase Order
purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice = get_mapped_purchase_invoice(po.name)
purchase_invoice.allocate_advances_automatically = 1 purchase_invoice.allocate_advances_automatically = 1
purchase_invoice.items[0].item_code = '_Test Non Stock Item'
purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC' purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
purchase_invoice.save() purchase_invoice.save()
purchase_invoice.submit() purchase_invoice.submit()

View File

@@ -989,7 +989,7 @@ class SalesInvoice(SellingController):
for payment_mode in self.payments: for payment_mode in self.payments:
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= self.change_amount payment_mode.base_amount -= flt(self.change_amount)
if payment_mode.amount: if payment_mode.amount:
# POS, make payment entries # POS, make payment entries

View File

@@ -1,24 +1,6 @@
// 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
cur_frm.add_fetch("customer", "customer_group", "customer_group" );
cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" );
frappe.ui.form.on("Tax Rule", "tax_type", function(frm) {
frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales");
frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase");
})
frappe.ui.form.on("Tax Rule", "onload", function(frm) {
if(frm.doc.__islocal) {
frm.set_value("use_for_shopping_cart", 1);
}
})
frappe.ui.form.on("Tax Rule", "refresh", function(frm) {
frappe.ui.form.trigger("Tax Rule", "tax_type");
})
frappe.ui.form.on("Tax Rule", "customer", function(frm) { frappe.ui.form.on("Tax Rule", "customer", function(frm) {
if(frm.doc.customer) { if(frm.doc.customer) {
frappe.call({ frappe.call({

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase):
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups", tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01") sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
tax_rule1.save() tax_rule1.save()
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}), self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
"_Test Sales Taxes and Charges Template - _TC") "_Test Sales Taxes and Charges Template - _TC")
def test_conflict_with_overlapping_dates(self): def test_conflict_with_overlapping_dates(self):

View File

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

View File

@@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry` from `tabGL Entry`
where party_type = %s and party=%s where party_type = %s and party=%s
and is_cancelled = 0
group by company""", (party_type, party))) group by company""", (party_type, party)))
for d in companies: for d in companies:

View File

@@ -222,7 +222,7 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters): def get_conditions(filters):
conditions = [] conditions = []
if filters.get("account") and not filters.get("include_dimensions"): if filters.get("account"):
filters.account = get_accounts_with_children(filters.account) filters.account = get_accounts_with_children(filters.account)
conditions.append("account in %(account)s") conditions.append("account in %(account)s")

View File

@@ -168,21 +168,24 @@ def get_columns(filters):
"label": _("Income"), "label": _("Income"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 305
}, },
{ {
"fieldname": "expense", "fieldname": "expense",
"label": _("Expense"), "label": _("Expense"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 305
}, },
{ {
"fieldname": "gross_profit_loss", "fieldname": "gross_profit_loss",
"label": _("Gross Profit / Loss"), "label": _("Gross Profit / Loss"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 307
} }
] ]

View File

@@ -9,13 +9,14 @@
"supp_master_name", "supp_master_name",
"supplier_group", "supplier_group",
"buying_price_list", "buying_price_list",
"maintain_same_rate_action",
"role_to_override_stop_action",
"column_break_3", "column_break_3",
"po_required", "po_required",
"pr_required", "pr_required",
"maintain_same_rate", "maintain_same_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
@@ -108,6 +109,13 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Role Allowed to Override Stop Action", "label": "Role Allowed to Override Stop Action",
"options": "Role" "options": "Role"
},
{
"default": "1",
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@@ -115,7 +123,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-04-04 20:01:44.087066", "modified": "2021-06-24 10:38:28.934525",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
} else { } else {
frm.set_value("tax_withholding_category", frm.supplier_tds); frm.set_value("tax_withholding_category", frm.supplier_tds);
} }
},
refresh: function(frm) {
frm.trigger('get_materials_from_supplier');
},
get_materials_from_supplier: function(frm) {
let po_details = [];
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name)
}
});
}
if (po_details && po_details.length) {
frm.add_custom_button(__('Return of Components'), () => {
frm.call({
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
freeze: true,
freeze_message: __('Creating Stock Entry'),
args: { purchase_order: frm.doc.name, po_details: po_details },
callback: function(r) {
if (r && r.message) {
const doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
}
});
}, __('Create'));
}
} }
}); });
@@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
}, },
has_unsupplied_items: function() { has_unsupplied_items: function() {
return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty) return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty)
}, },
make_stock_entry: function() { make_stock_entry: function() {
@@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
], ],
primary_action: function() { primary_action: function() {
var data = d.get_values(); var data = d.get_values();
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
frappe.call({ frappe.call({
method: "frappe.desk.form.utils.add_comment", method: "frappe.desk.form.utils.add_comment",
args: { args: {
reference_doctype: me.frm.doctype, reference_doctype: me.frm.doctype,
reference_name: me.frm.docname, reference_name: me.frm.docname,
content: __('Reason for hold:') + " " +data.reason_for_hold, content: __(reason_for_hold),
comment_email: frappe.session.user, comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname comment_by: frappe.session.user_fullname
}, },

View File

@@ -609,6 +609,7 @@
"fieldname": "supplied_items", "fieldname": "supplied_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Supplied Items", "label": "Supplied Items",
"no_copy": 1,
"oldfieldname": "po_raw_material_details", "oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Purchase Order Item Supplied", "options": "Purchase Order Item Supplied",
@@ -1377,7 +1378,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 00:55:30.781375", "modified": "2021-05-30 15:17:53.663648",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency from erpnext.accounts.party import get_party_account_currency
from six import string_types
from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
unlink_inter_company_doc update_linked_doc, unlink_inter_company_doc)
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
@@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
@frappe.whitelist() @frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items): def make_rm_stock_entry(purchase_order, rm_items):
if isinstance(rm_items, string_types): rm_items_list = rm_items
if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items) rm_items_list = json.loads(rm_items)
else: elif not rm_items:
frappe.throw(_("No Items available for transfer")) frappe.throw(_("No Items available for transfer"))
if rm_items_list: if rm_items_list:
@@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
'qty': rm_item_data["qty"], 'qty': rm_item_data["qty"],
'from_warehouse': rm_item_data["warehouse"], 'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"], 'stock_uom': rm_item_data["stock_uom"],
'serial_no': rm_item_data.get('serial_no'),
'batch_no': rm_item_data.get('batch_no'),
'main_item_code': rm_item_data["item_code"], 'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item') 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
} }
@@ -582,3 +585,58 @@ def update_status(status, name):
def make_inter_company_sales_order(source_name, target_doc=None): def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc) return make_inter_company_transaction("Purchase Order", source_name, target_doc)
@frappe.whitelist()
def get_materials_from_supplier(purchase_order, po_details):
if isinstance(po_details, str):
po_details = json.loads(po_details)
doc = frappe.get_cached_doc('Purchase Order', purchase_order)
doc.initialized_fields()
doc.purchase_orders = [doc.name]
doc.get_available_materials()
if not doc.available_materials:
frappe.throw(_('Materials are already received against the purchase order {0}')
.format(purchase_order))
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
ste_doc = frappe.new_doc('Stock Entry')
ste_doc.purpose = 'Material Transfer'
ste_doc.purchase_order = po_doc.name
ste_doc.company = po_doc.company
ste_doc.is_return = 1
for key, value in available_materials.items():
if not value.qty:
continue
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, po_details)
ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()
return ste_doc
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
item = ste_doc.append('items', row.item_details)
po_detail = list(set(row.po_details).intersection(po_details))
item.update({
'qty': qty,
'batch_no': batch_no,
'basic_rate': row.item_details['rate'],
'po_detail': po_detail[0] if po_detail else '',
's_warehouse': row.item_details['t_warehouse'],
't_warehouse': row.item_details['s_warehouse'],
'item_code': row.item_details['rm_item_code'],
'subcontracted_item': row.item_details['main_item_code'],
'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
})

View File

@@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.batch.test_batch import make_new_batch
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase): class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self): def test_make_purchase_receipt(self):
@@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items_in_subcontracted(self): def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code) make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1, po = create_purchase_order(item_code=item_code, qty=1,
@@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
for item in rm_items: for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item transferred_rm_map[item.get('rm_item_code')] = item
for item in pr.get('supplied_items'):
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
update_backflush_based_on("BOM")
def test_backflushed_based_on_for_multiple_batches(self):
item_code = "_Test Subcontracted FG Item 2"
make_item('Sub Contracted Raw Material 2', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
raw_materials=["Sub Contracted Raw Material 2"])
update_backflush_based_on("Material Transferred for Subcontract")
order_qty = 500
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
rm_items = [
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
make_new_batch(batch_id=batch, item_code=item_code)
pr = make_purchase_receipt(po.name)
# partial receipt
pr.get('items')[0].qty = 30
pr.get('items')[0].batch_no = "ABCD1"
purchase_order = po.name
purchase_order_item = po.items[0].name
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
pr.append("items", {
"item_code": pr.get('items')[0].item_code,
"item_name": pr.get('items')[0].item_name,
"uom": pr.get('items')[0].uom,
"stock_uom": pr.get('items')[0].stock_uom,
"warehouse": pr.get('items')[0].warehouse,
"conversion_factor": pr.get('items')[0].conversion_factor,
"cost_center": pr.get('items')[0].cost_center,
"rate": pr.get('items')[0].rate,
"qty": qty,
"batch_no": batch_no,
"purchase_order": purchase_order,
"purchase_order_item": purchase_order_item
})
pr.submit()
pr1 = make_purchase_receipt(po.name)
pr1.get('items')[0].qty = 300
pr1.get('items')[0].batch_no = "ABCD1"
pr1.save()
pr_key = ("Sub Contracted Raw Material 2", po.name)
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
update_backflush_based_on("BOM") update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self): def test_supplied_qty_against_subcontracted_po(self):
@@ -1117,22 +1043,29 @@ def create_purchase_order(**args):
po.conversion_factor = args.conversion_factor or 1 po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None po.supplier_warehouse = args.supplier_warehouse or None
po.append("items", { if args.rm_items:
"item_code": args.item or args.item_code or "_Test Item", for row in args.rm_items:
"warehouse": args.warehouse or "_Test Warehouse - _TC", po.append("items", row)
"qty": args.qty or 10, else:
"rate": args.rate or 500, po.append("items", {
"schedule_date": add_days(nowdate(), 1), "item_code": args.item or args.item_code or "_Test Item",
"include_exploded_items": args.get('include_exploded_items', 1), "warehouse": args.warehouse or "_Test Warehouse - _TC",
"against_blanket_order": args.against_blanket_order "qty": args.qty or 10,
}) "rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get('include_exploded_items', 1),
"against_blanket_order": args.against_blanket_order
})
po.set_missing_values()
if not args.do_not_save: if not args.do_not_save:
po.insert() po.insert()
if not args.do_not_submit: if not args.do_not_submit:
if po.is_subcontracted == "Yes": if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items") supp_items = po.get("supplied_items")
for d in supp_items: for d in supp_items:
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" if not d.reserve_warehouse:
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
po.submit() po.submit()
return po return po

View File

@@ -97,6 +97,9 @@
"is_fixed_asset", "is_fixed_asset",
"item_tax_rate", "item_tax_rate",
"section_break_72", "section_break_72",
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
"page_break" "page_break"
], ],
"fields": [ "fields": [
@@ -803,13 +806,37 @@
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "production_plan",
"fieldtype": "Link",
"label": "Production Plan",
"options": "Production Plan",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "production_plan_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Production Plan Item",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "production_plan_sub_assembly_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Production Plan Sub Assembly Item",
"no_copy": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-03-22 11:46:12.357435", "modified": "2021-06-28 19:22:22.715365",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@@ -6,21 +6,25 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"main_item_code", "main_item_code",
"bom_detail_no", "rm_item_code",
"column_break_3",
"stock_uom", "stock_uom",
"reserve_warehouse",
"conversion_factor", "conversion_factor",
"column_break_6", "column_break_6",
"rm_item_code", "bom_detail_no",
"reference_name", "reference_name",
"reserve_warehouse",
"section_break2", "section_break2",
"rate", "rate",
"col_break2", "col_break2",
"amount", "amount",
"section_break1", "section_break1",
"required_qty", "required_qty",
"supplied_qty",
"col_break1", "col_break1",
"supplied_qty" "consumed_qty",
"returned_qty",
"total_supplied_qty"
], ],
"fields": [ "fields": [
{ {
@@ -125,6 +129,8 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Supplied Qty", "label": "Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -142,13 +148,42 @@
{ {
"fieldname": "col_break2", "fieldname": "col_break2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "consumed_qty",
"fieldtype": "Float",
"label": "Consumed Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "total_supplied_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Total Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-06-09 15:17:58.128242",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item Supplied", "name": "Purchase Order Item Supplied",

View File

@@ -6,10 +6,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"main_item_code", "main_item_code",
"description", "rm_item_code",
"item_name",
"bom_detail_no", "bom_detail_no",
"col_break1", "col_break1",
"rm_item_code", "description",
"stock_uom", "stock_uom",
"conversion_factor", "conversion_factor",
"reference_name", "reference_name",
@@ -25,7 +26,8 @@
"secbreak_3", "secbreak_3",
"batch_no", "batch_no",
"col_break4", "col_break4",
"serial_no" "serial_no",
"purchase_order"
], ],
"fields": [ "fields": [
{ {
@@ -52,7 +54,6 @@
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1,
"label": "Description", "label": "Description",
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@@ -81,18 +82,20 @@
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Required Qty", "label": "Available Qty For Consumption",
"oldfieldname": "required_qty", "oldfieldname": "required_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 2,
"fieldname": "consumed_qty", "fieldname": "consumed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Consumed Qty", "in_list_view": 1,
"label": "Qty to Be Consumed",
"oldfieldname": "consumed_qty", "oldfieldname": "consumed_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@@ -183,12 +186,28 @@
{ {
"fieldname": "col_break4", "fieldname": "col_break4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "purchase_order",
"fieldtype": "Link",
"hidden": 1,
"label": "Purchase Order",
"no_copy": 1,
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-06-19 19:33:04.431213",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Receipt Item Supplied", "name": "Purchase Receipt Item Supplied",

View File

@@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
create_rfq_items(sq_doc, supplier, data) create_rfq_items(sq_doc, supplier, data)
def create_rfq_items(sq_doc, supplier, data): def create_rfq_items(sq_doc, supplier, data):
sq_doc.append('items', { args = {}
"item_code": data.item_code,
"item_name": data.item_name, for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
"description": data.description, 'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
"qty": data.qty, args[field] = data.get(field)
"rate": data.rate,
"conversion_factor": data.conversion_factor if data.conversion_factor else None, args.update({
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
"warehouse": data.warehouse or '',
"request_for_quotation_item": data.name, "request_for_quotation_item": data.name,
"request_for_quotation": data.parent "request_for_quotation": data.parent,
"supplier_part_no": frappe.db.get_value("Item Supplier",
{'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
}) })
sq_doc.append('items', args)
@frappe.whitelist() @frappe.whitelist()
def get_pdf(doctype, name, supplier): def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(doctype, name, supplier) doc = get_rfq_doc(doctype, name, supplier)

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Subcontract Order Summary"] = {
"filters": [
{
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
label: __("From Date"),
fieldname:"from_date",
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
label: __("To Date"),
fieldname:"to_date",
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1
},
{
label: __("Purchase Order"),
fieldname: "name",
fieldtype: "Link",
options: "Purchase Order",
get_query: function() {
return {
filters: {
docstatus: 1,
is_subcontracted: 'Yes',
company: frappe.query_report.get_filter_value('company')
}
}
}
}
]
};

View File

@@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-05-31 14:43:32.417694",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-05-31 14:43:32.417694",
"modified_by": "Administrator",
"module": "Buying",
"name": "Subcontract Order Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"report_name": "Subcontract Order Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
}
]
}

View File

@@ -0,0 +1,152 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
columns, data = [], []
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(report_filters):
data = []
orders = get_subcontracted_orders(report_filters)
if orders:
supplied_items = get_supplied_items(orders, report_filters)
po_details = prepare_subcontracted_data(orders, supplied_items)
get_subcontracted_data(po_details, data)
return data
def get_subcontracted_orders(report_filters):
fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
'`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
'`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
filters = get_filters(report_filters)
return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
def get_filters(report_filters):
filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
for field in ['name', 'company']:
if report_filters.get(field):
filters.append(['Purchase Order', field, '=', report_filters.get(field)])
return filters
def get_supplied_items(orders, report_filters):
if not orders:
return []
fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
supplied_items = {}
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
new_key = (row.parent, row.reference_name, row.main_item_code)
supplied_items.setdefault(new_key, []).append(row)
return supplied_items
def prepare_subcontracted_data(orders, supplied_items):
po_details = {}
for row in orders:
key = (row.po_id, row.name, row.item_code)
if key not in po_details:
po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
details = po_details[key]
if supplied_items.get(key):
for supplied_item in supplied_items[key]:
details['supplied_items'].append(supplied_item)
return po_details
def get_subcontracted_data(po_details, data):
for key, details in po_details.items():
res = details.po_item
for index, row in enumerate(details.supplied_items):
if index != 0:
res = {}
res.update(row)
data.append(res)
def get_columns():
return [
{
"label": _("Purchase Order"),
"fieldname": "po_id",
"fieldtype": "Link",
"options": "Purchase Order",
"width": 100
},
{
"label": _("Status"),
"fieldname": "status",
"fieldtype": "Data",
"width": 80
},
{
"label": _("Subcontracted Item"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 160
},
{
"label": _("Order Qty"),
"fieldname": "qty",
"fieldtype": "Float",
"width": 90
},
{
"label": _("Received Qty"),
"fieldname": "received_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Supplied Item"),
"fieldname": "rm_item_code",
"fieldtype": "Link",
"options": "Item",
"width": 160
},
{
"label": _("Required Qty"),
"fieldname": "required_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Supplied Qty"),
"fieldname": "supplied_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Consumed Qty"),
"fieldname": "consumed_qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("Returned Qty"),
"fieldname": "returned_qty",
"fieldtype": "Float",
"width": 110
}
]

View File

@@ -828,8 +828,14 @@ class AccountsController(TransactionBase):
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles(): if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") if self.doctype != "Purchase Invoice":
.format(item.item_code, item.idx, max_allowed_amt)) self.throw_overbill_exception(item, max_allowed_amt)
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
self.throw_overbill_exception(item, max_allowed_amt)
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname): def get_company_default(self, fieldname):
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default

View File

@@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
class BuyingController(StockController): from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.subcontracting import Subcontracting
class BuyingController(StockController, Subcontracting):
def get_feed(self): def get_feed(self):
if self.get("supplier_name"): if self.get("supplier_name"):
@@ -57,6 +58,11 @@ class BuyingController(StockController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"): if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate() self.update_valuation_rate()
def onload(self):
super(BuyingController, self).onload()
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
'backflush_raw_materials_of_subcontract_based_on'))
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate) super(BuyingController, self).set_missing_values(for_validate)
@@ -171,12 +177,13 @@ class BuyingController(StockController):
TODO: rename item_tax_amount to valuation_tax_amount TODO: rename item_tax_amount to valuation_tax_amount
""" """
stock_and_asset_items = []
stock_and_asset_items = self.get_stock_items() + self.get_asset_items() stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1 last_item_idx = 1
for d in self.get("items"): for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items: if (d.item_code and d.item_code in stock_and_asset_items):
stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount) stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx last_item_idx = d.idx
@@ -255,7 +262,7 @@ class BuyingController(StockController):
supplied_items_cost = 0.0 supplied_items_cost = 0.0
for d in self.get("supplied_items"): for d in self.get("supplied_items"):
if d.reference_name == item_row_id: if d.reference_name == item_row_id:
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'): if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
rate = get_incoming_rate({ rate = get_incoming_rate({
"item_code": d.rm_item_code, "item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse, "warehouse": self.supplier_warehouse,
@@ -285,11 +292,13 @@ class BuyingController(StockController):
if item in self.sub_contracted_items and not item.bom: if item in self.sub_contracted_items and not item.bom:
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
if self.doctype == "Purchase Order": if self.doctype != "Purchase Order":
for supplied_item in self.get("supplied_items"): return
if not supplied_item.reserve_warehouse:
frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
for row in self.get("supplied_items"):
if not row.reserve_warehouse:
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
frappe.throw(_(msg))
else: else:
for item in self.get("items"): for item in self.get("items"):
if item.bom: if item.bom:
@@ -297,23 +306,7 @@ class BuyingController(StockController):
def create_raw_materials_supplied(self, raw_material_table): def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted=="Yes": if self.is_subcontracted=="Yes":
parent_items = [] self.set_materials_for_subcontracted_items(raw_material_table)
backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
"backflush_raw_materials_of_subcontract_based_on")
if (self.doctype == 'Purchase Receipt' and
backflush_raw_materials_based_on != 'BOM'):
self.update_raw_materials_supplied_based_on_stock_entries()
else:
for item in self.get("items"):
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
item.rm_supp_cost = 0.0
if item.bom and item.item_code in self.sub_contracted_items:
self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
if [item.item_code, item.name] not in parent_items:
parent_items.append([item.item_code, item.name])
self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"): for item in self.get("items"):
@@ -322,176 +315,6 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"): if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', []) self.set('supplied_items', [])
def update_raw_materials_supplied_based_on_stock_entries(self):
self.set('supplied_items', [])
purchase_orders = set(d.purchase_order for d in self.items)
# qty of raw materials backflushed (for each item per purchase order)
backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
# qty of "finished good" item yet to be received
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
for item in self.get('items'):
if not item.purchase_order:
continue
# reset raw_material cost
item.rm_supp_cost = 0
# qty of raw materials transferred to the supplier
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
item_key = '{}{}'.format(item.item_code, item.purchase_order)
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
if not fg_yet_to_be_received:
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
.format(item.idx, frappe.bold(item.item_code),
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty
rm_qty_to_be_consumed = transferred_qty - consumed_qty
# backflush all remaining transferred qty in the last Purchase Receipt
if fg_yet_to_be_received == item.qty:
qty = rm_qty_to_be_consumed
else:
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
qty = frappe.utils.ceil(qty)
if qty > rm_qty_to_be_consumed:
qty = rm_qty_to_be_consumed
if not qty: continue
if raw_material.serial_nos:
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
if qty > 0:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
if not rm.main_item_code:
rm.main_item_code = fg_item_row.item_code
rm.reference_name = fg_item_row.name
rm.required_qty = qty
rm.consumed_qty = qty
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
exploded_item = item.get('include_exploded_items')
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = []
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
raw_materials_cost = 0
items = list(set([d.item_code for d in bom_items]))
item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
from `tabItem` i, `tabItem Default` id
where id.parent=i.name and id.company=%s and i.name in ({0})"""
.format(", ".join(["%s"] * len(items))), [self.company] + items))
for bom_item in bom_items:
if self.doctype == "Purchase Order":
reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
reserve_warehouse = None
conversion_factor = item.conversion_factor
if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
bom_item.item_code in used_alternative_items):
alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code
bom_item.item_name = alternative_item_data.item_name
bom_item.stock_uom = alternative_item_data.stock_uom
conversion_factor = alternative_item_data.conversion_factor
bom_item.description = alternative_item_data.description
# check if exists
exists = 0
for d in self.get(raw_material_table):
if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
and d.reference_name == item.name:
rm, exists = d, 1
break
if not exists:
rm = self.append(raw_material_table, {})
required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
flt(conversion_factor), rm.precision("required_qty"))
rm.reference_name = item.name
rm.bom_detail_no = bom_item.name
rm.main_item_code = item.item_code
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
rm.consumed_qty = required_qty
rm.description = bom_item.description
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
rm.batch_no = item.batch_no
elif not rm.reserve_warehouse:
rm.reserve_warehouse = reserve_warehouse
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
"""Remove all those child items which are no longer present in main item table"""
delete_list = []
for d in self.get(raw_material_table):
if [d.main_item_code, d.reference_name] not in parent_items:
# mark for deletion from doclist
delete_list.append(d)
# delete from doclist
if delete_list:
rm_supplied_details = self.get(raw_material_table)
self.set(raw_material_table, [])
for d in rm_supplied_details:
if d not in delete_list:
self.append(raw_material_table, d)
@property @property
def sub_contracted_items(self): def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"): if not hasattr(self, "_sub_contracted_items"):
@@ -683,7 +506,8 @@ class BuyingController(StockController):
self.process_fixed_asset() self.process_fixed_asset()
self.update_fixed_asset(field) self.update_fixed_asset(field)
update_last_purchase_rate(self, is_submit = 1) if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 1)
def on_cancel(self): def on_cancel(self):
super(BuyingController, self).on_cancel() super(BuyingController, self).on_cancel()
@@ -691,7 +515,9 @@ class BuyingController(StockController):
if self.get('is_return'): if self.get('is_return'):
return return
update_last_purchase_rate(self, is_submit = 0) if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 0)
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
@@ -863,104 +689,6 @@ class BuyingController(StockController):
else: else:
validate_item_type(self, "is_purchase_item", "purchase") validate_item_type(self, "is_purchase_item", "purchase")
def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
bom_items = frappe.db.sql("""select t2.item_code, t2.name,
t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
from
`tabBOM` t1, `tab{0}` t2, tabItem t3
where
t2.parent = t1.name and t1.item = %s
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
and t2.sourced_by_supplier = 0
and t2.item_code = t3.name""".format(doctype),
(item_code, bom), as_dict=1)
if not bom_items:
msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
return bom_items
def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
common_query = """
SELECT
sed.item_code AS rm_item_code,
SUM(sed.qty) AS qty,
sed.description,
sed.stock_uom,
sed.subcontracted_item AS main_item_code,
{serial_no_concat_syntax} AS serial_nos,
{batch_no_concat_syntax} AS batch_nos
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
)
}, (purchase_order, fg_item), as_dict=1)
return raw_materials
def get_backflushed_subcontracted_raw_materials(purchase_orders):
purchase_receipts = frappe.get_all("Purchase Receipt Item",
fields = ["purchase_order", "item_code", "name", "parent"],
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
distinct_purchase_receipts = {}
for pr in purchase_receipts:
key = (pr.purchase_order, pr.item_code, pr.parent)
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
backflushed_raw_materials_map = frappe._dict()
for args, references in iteritems(distinct_purchase_receipts):
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
for data in purchase_receipt_supplied_items:
pr_key = (data.rm_item_code, data.main_item_code, args[0])
if pr_key not in backflushed_raw_materials_map:
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
"qty": 0.0,
"serial_no": [],
"batch_no": [],
"consumed_batch": {}
}))
row = backflushed_raw_materials_map.get(pr_key)
row.qty += data.consumed_qty
for field in ["serial_no", "batch_no"]:
if data.get(field):
row[field].append(data.get(field))
if data.get("batch_no"):
if data.get("batch_no") in row.consumed_batch:
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
else:
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
return backflushed_raw_materials_map
def get_supplied_items(item_code, purchase_receipt, references):
return frappe.get_all("Purchase Receipt Item Supplied",
fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
def get_asset_item_details(asset_items): def get_asset_item_details(asset_items):
asset_items_data = {} asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message) error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
frappe.throw(error_message) frappe.throw(error_message)
def get_qty_to_be_received(purchase_orders):
return frappe._dict(frappe.db.sql("""
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
FROM `tabPurchase Order Item` poi
WHERE
poi.`parent` in %s
GROUP BY poi.`item_code`, poi.`parent`
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
""", (purchase_orders)))
def get_non_stock_items(purchase_order, fg_item_code):
return frappe.db.sql("""
SELECT
pois.main_item_code,
pois.rm_item_code,
item.description,
pois.required_qty AS qty,
pois.rate,
1 as non_stock_item,
pois.stock_uom
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
WHERE
pois.`rm_item_code` = item.`name`
AND item.is_stock_item = 0
AND pois.`parent` = %s
AND pois.`main_item_code` = %s
""", (purchase_order, fg_item_code), as_dict=1)
def set_serial_nos(raw_material, consumed_serial_nos, qty):
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
set(get_serial_nos(consumed_serial_nos))
if serial_nos and qty <= len(serial_nos):
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
def get_transferred_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
transferred_batch_qty_map = {}
transferred_batches = frappe.db.sql("""
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
sed.item_code,
sed.subcontracted_item
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND ifnull(sed.subcontracted_item, '') in ('', %s)
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
sed.item_code
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
key = ((batch_data.item_code, fg_item)
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
transferred_batch_qty_map.setdefault(key, OrderedDict())
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
def get_backflushed_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
backflushed_batch_qty_map = {}
backflushed_batches = frappe.db.sql("""
SELECT
pris.batch_no,
SUM(pris.consumed_qty) AS qty,
pris.rm_item_code AS item_code
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
WHERE
pr.name = pri.parent
AND pri.parent = pris.parent
AND pri.purchase_order = %s
AND pri.item_code = pris.main_item_code
AND pr.docstatus = 1
AND pris.main_item_code = %s
AND pris.batch_no IS NOT NULL
GROUP BY
pris.rm_item_code, pris.batch_no
""", (purchase_order, fg_item), as_dict=1)
for batch_data in backflushed_batches:
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
return backflushed_batch_qty_map
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
if not transferred_batches:
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
available_batches = []
for (batch, transferred_qty) in transferred_batches.items():
backflushed_qty = backflushed_batches.get(batch, 0)
available_qty = transferred_qty - backflushed_qty
if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty})
break
elif available_qty != 0:
available_batches.append({'batch': batch, 'qty': available_qty})
required_qty -= available_qty
for row in available_batches:
if backflushed_batches.get(row.get('batch'), 0) > 0:
backflushed_batches[row.get('batch')] += row.get('qty')
else:
backflushed_batches[row.get('batch')] = row.get('qty')
return available_batches

View File

@@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
fields = get_fields("Employee", ["name", "employee_name"]) fields = get_fields("Employee", ["name", "employee_name"])
return frappe.db.sql("""select {fields} from `tabEmployee` return frappe.db.sql("""select {fields} from `tabEmployee`
where status = 'Active' where status in ('Active', 'Suspended')
and docstatus < 2 and docstatus < 2
and ({key} like %(txt)s and ({key} like %(txt)s
or employee_name like %(txt)s) or employee_name like %(txt)s)
@@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select {fields} from `tabProject` return frappe.db.sql("""select {fields} from `tabProject`
where where
`tabProject`.status not in ("Completed", "Cancelled") `tabProject`.status not in ("Completed", "Cancelled")
and {cond} {match_cond} {scond} and {cond} {scond} {match_cond}
order by order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc, idx desc,

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
import erpnext import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
@@ -497,10 +497,6 @@ class StockController(AccountsController):
}) })
if future_sle_exists(args): if future_sle_exists(args):
create_repost_item_valuation_entry(args) create_repost_item_valuation_entry(args)
elif not is_reposting_pending():
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
@frappe.whitelist() @frappe.whitelist()
def make_quality_inspections(doctype, docname, items): def make_quality_inspections(doctype, docname, items):
@@ -533,21 +529,75 @@ def make_quality_inspections(doctype, docname, items):
return inspections return inspections
def is_reposting_pending(): def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation", return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
def future_sle_exists(args, sl_entries=None):
key = (args.voucher_type, args.voucher_no)
def future_sle_exists(args): if validate_future_sle_not_exists(args, key, sl_entries):
sl_entries = frappe.get_all("Stock Ledger Entry", return False
elif get_cached_data(args, key):
return True
if not sl_entries:
sl_entries = get_sle_entries_against_voucher(args)
if not sl_entries:
return
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
data = frappe.db.sql("""
select item_code, warehouse, count(name) as total_row
from `tabStock Ledger Entry`
where
({})
and timestamp(posting_date, posting_time)
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
GROUP BY
item_code, warehouse
""".format(" or ".join(or_conditions)), args, as_dict=1)
for d in data:
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data)
def validate_future_sle_not_exists(args, key, sl_entries=None):
item_key = ''
if args.get('item_code'):
item_key = (args.get('item_code'), args.get('warehouse'))
if not sl_entries and hasattr(frappe.local, 'future_sle'):
if (not frappe.local.future_sle.get(key) or
(item_key and item_key not in frappe.local.future_sle.get(key))):
return True
def get_cached_data(args, key):
if not hasattr(frappe.local, 'future_sle'):
frappe.local.future_sle = {}
if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({})
if args.get('item_code'):
item_key = (args.get('item_code'), args.get('warehouse'))
count = frappe.local.future_sle[key].get(item_key)
return True if (count or count == 0) else False
else:
return frappe.local.future_sle[key]
def get_sle_entries_against_voucher(args):
return frappe.get_all("Stock Ledger Entry",
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"], fields=["item_code", "warehouse"],
order_by="creation asc") order_by="creation asc")
if not sl_entries: def get_conditions_to_validate_future_sle(sl_entries):
return
warehouse_items_map = {} warehouse_items_map = {}
for entry in sl_entries: for entry in sl_entries:
if entry.warehouse not in warehouse_items_map: if entry.warehouse not in warehouse_items_map:
@@ -561,17 +611,7 @@ def future_sle_exists(args):
f"""warehouse = {frappe.db.escape(warehouse)} f"""warehouse = {frappe.db.escape(warehouse)}
and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
return frappe.db.sql(""" return or_conditions
select name
from `tabStock Ledger Entry`
where
({})
and timestamp(posting_date, posting_time)
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
limit 1
""".format(" or ".join(or_conditions)), args)
def create_repost_item_valuation_entry(args): def create_repost_item_valuation_entry(args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -0,0 +1,393 @@
import frappe
import copy
from frappe import _
from frappe.utils import flt, cint, get_link_to_form
from collections import defaultdict
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class Subcontracting():
def set_materials_for_subcontracted_items(self, raw_material_table):
if self.doctype == 'Purchase Invoice' and not self.update_stock:
return
self.raw_material_table = raw_material_table
self.__identify_change_in_item_table()
self.__prepare_supplied_items()
self.__validate_supplied_items()
def __prepare_supplied_items(self):
self.initialized_fields()
self.__get_purchase_orders()
self.__get_pending_qty_to_receive()
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
def initialized_fields(self):
self.available_materials = frappe._dict()
self.__transferred_items = frappe._dict()
self.alternative_item_details = frappe._dict()
self.__get_backflush_based_on()
def __get_backflush_based_on(self):
self.backflush_based_on = frappe.db.get_single_value("Buying Settings",
"backflush_raw_materials_of_subcontract_based_on")
def __get_purchase_orders(self):
self.purchase_orders = []
if self.doctype == 'Purchase Order':
return
self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
def __identify_change_in_item_table(self):
self.__changed_name = []
self.__reference_name = []
if self.doctype == 'Purchase Order' or self.is_new():
self.set(self.raw_material_table, [])
return
item_dict = self.__get_data_before_save()
if not item_dict:
return True
for n_row in self.items:
self.__reference_name.append(n_row.name)
if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
self.__changed_name.append(n_row.name)
if item_dict.get(n_row.name):
del item_dict[n_row.name]
self.__changed_name.extend(item_dict.keys())
def __get_data_before_save(self):
item_dict = {}
if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save:
for row in self._doc_before_save.get('items'):
item_dict[row.name] = (row.item_code, row.qty)
return item_dict
def get_available_materials(self):
''' Get the available raw materials which has been transferred to the supplier.
available_materials = {
(item_code, subcontracted_item, purchase_order): {
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
}
}
'''
if not self.purchase_orders:
return
for row in self.__get_transferred_items():
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if key not in self.available_materials:
self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [],
'batch_no': defaultdict(float), 'item_details': row, 'po_details': []})
)
details = self.available_materials[key]
details.qty += row.qty
details.po_details.append(row.po_detail)
if row.serial_no:
details.serial_no.extend(get_serial_nos(row.serial_no))
if row.batch_no:
details.batch_no[row.batch_no] += row.qty
self.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials)
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
self.__update_consumed_materials(doctype)
def __update_consumed_materials(self, doctype, return_consumed_items=False):
'''Deduct the consumed materials from the available materials.'''
pr_items = self.__get_received_items(doctype)
if not pr_items:
return ([], {}) if return_consumed_items else None
pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items}
consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
if return_consumed_items:
return (consumed_materials, pr_items)
for row in consumed_materials:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
if not self.available_materials.get(key):
continue
self.available_materials[key]['qty'] -= row.consumed_qty
if row.serial_no:
self.available_materials[key]['serial_no'] = list(
set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no))
)
if row.batch_no:
self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty
def __get_transferred_items(self):
fields = ['`tabStock Entry`.`purchase_order`']
alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'}
child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount',
'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor',
's_warehouse', 't_warehouse', 'item_group', 'po_detail']
if self.backflush_based_on == 'BOM':
child_table_fields.append('original_item')
for field in child_table_fields:
fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}')
filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'],
['Stock Entry', 'purchase_order', 'in', self.purchase_orders]]
return frappe.get_all('Stock Entry', fields = fields, filters=filters)
def __get_received_items(self, doctype):
fields = []
self.po_field = 'purchase_order'
for field in ['name', self.po_field, 'parent']:
fields.append(f'`tab{doctype} Item`.`{field}`')
filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]]
if doctype == 'Purchase Invoice':
filters.append(['Purchase Invoice', 'update_stock', "=", 1])
return frappe.get_all(f'{doctype}', fields = fields, filters = filters)
def __get_consumed_items(self, doctype, pr_items):
return frappe.get_all('Purchase Receipt Item Supplied',
fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'],
filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype})
def __set_alternative_item_details(self, row):
if row.get('original_item'):
self.alternative_item_details[row.get('original_item')] = row
def __get_pending_qty_to_receive(self):
'''Get qty to be received against the purchase order.'''
self.qty_to_be_received = defaultdict(float)
if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders:
for row in frappe.get_all('Purchase Order Item',
fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'],
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item'
fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit']
alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'}
for field in ['item_code', 'name', 'rate', 'stock_uom',
'source_warehouse', 'description', 'item_name', 'stock_uom']:
fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}')
filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1],
['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]]
return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or []
def __remove_changed_rows(self):
if not self.__changed_name:
return
i=1
self.set(self.raw_material_table, [])
for d in self._doc_before_save.supplied_items:
if d.reference_name in self.__changed_name:
continue
if (d.reference_name not in self.__reference_name):
continue
d.idx = i
self.append('supplied_items', d)
i += 1
def __set_supplied_items(self):
self.bom_items = {}
has_supplied_items = True if self.get(self.raw_material_table) else False
for row in self.items:
if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name)
or (has_supplied_items and not self.__changed_name))):
continue
if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM':
for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')):
qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor)
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)
self.__add_supplied_item(row, bom_item, qty)
elif self.backflush_based_on != 'BOM':
for key, transfer_item in self.available_materials.items():
if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
transfer_item.qty -= qty
self.__add_supplied_item(row, transfer_item.get('item_details'), qty)
if self.qty_to_be_received:
self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
def __update_reserve_warehouse(self, row, item):
if self.doctype == 'Purchase Order':
row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.purchase_order)
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
if self.qty_to_be_received:
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
transfer_item.item_details.required_qty = transfer_item.qty
if (transfer_item.serial_no or frappe.get_cached_value('UOM',
transfer_item.item_details.stock_uom, 'must_be_whole_number')):
return frappe.utils.ceil(qty)
return qty
def __set_alternative_item(self, bom_item):
if self.alternative_item_details.get(bom_item.rm_item_code):
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name
if self.doctype == 'Purchase Order':
rm_obj.required_qty = qty
else:
rm_obj.consumed_qty = 0
rm_obj.purchase_order = item_row.purchase_order
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if (self.available_materials.get(key) and self.available_materials[key]['batch_no']):
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]['batch_no'].items():
if batch_qty >= qty:
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]['batch_no'][batch_no] -= qty
return
elif qty > 0 and batch_qty > 0:
qty -= batch_qty
new_rm_obj = self.append(self.raw_material_table, bom_item)
new_rm_obj.reference_name = item_row.name
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]['batch_no'][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no,
'required_qty': qty, 'purchase_order': item_row.purchase_order})
self.__set_serial_nos(item_row, rm_obj)
def __set_serial_nos(self, item_row, rm_obj):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if (self.available_materials.get(key) and self.available_materials[key]['serial_no']):
used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)]
rm_obj.serial_no = '\n'.join(used_serial_nos)
# Removed the used serial nos from the list
for sn in used_serial_nos:
self.available_materials[key]['serial_no'].remove(sn)
def set_consumed_qty_in_po(self):
# Update consumed qty back in the purchase order
if self.is_subcontracted != 'Yes':
return
self.__get_purchase_orders()
itemwise_consumed_qty = defaultdict(float)
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
for row in consumed_items:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
itemwise_consumed_qty[key] += row.consumed_qty
self.__update_consumed_qty_in_po(itemwise_consumed_qty)
def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name']
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'):
key = (row.rm_item_code, row.main_item_code, row.parent)
consumed_qty = itemwise_consumed_qty.get(key, 0)
if row.supplied_qty < consumed_qty:
consumed_qty = row.supplied_qty
itemwise_consumed_qty[key] -= consumed_qty
frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty)
def __validate_supplied_items(self):
if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']:
return
for row in self.get(self.raw_material_table):
self.__validate_consumed_qty(row)
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key):
return
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def __validate_consumed_qty(self, row):
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
def __validate_batch_no(self, row, key):
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
link = get_link_to_form('Purchase Order', row.purchase_order)
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get('serial_no'):
serial_nos = get_serial_nos(row.get('serial_no'))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no'))
if incorrect_sn:
incorrect_sn = "\n".join(incorrect_sn)
link = get_link_to_form('Purchase Order', row.purchase_order)
msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}'
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))

View File

@@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object):
item.margin_type = None item.margin_type = None
item.margin_rate_or_amount = 0.0 item.margin_rate_or_amount = 0.0
if item.margin_type and item.margin_rate_or_amount: if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
item.margin_type = "Amount"
item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
item.precision("margin_rate_or_amount"))
item.rate_with_margin = item.rate
elif item.margin_type and item.margin_rate_or_amount:
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
rate_with_margin = flt(item.price_list_rate) + flt(margin_value) rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)

View File

@@ -102,7 +102,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-01-28 16:16:45.447213", "modified": "2021-06-29 18:27:02.832979",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Appointment", "name": "Appointment",
@@ -153,6 +153,18 @@
"role": "Sales User", "role": "Sales User",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,

View File

@@ -168,12 +168,13 @@ class Lead(SellingController):
if self.phone: if self.phone:
contact.append("phone_nos", { contact.append("phone_nos", {
"phone": self.phone, "phone": self.phone,
"is_primary": 1 "is_primary_phone": 1
}) })
if self.mobile_no: if self.mobile_no:
contact.append("phone_nos", { contact.append("phone_nos", {
"phone": self.mobile_no "phone": self.mobile_no,
"is_primary_mobile_no":1
}) })
contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)

View File

@@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
get_chart_data: function (_columns, result) { get_chart_data: function (_columns, result) {
return { return {
data: { data: {
labels: result.map(d => d[0]), labels: result.map(d => d.creation_date),
datasets: [{ datasets: [{
name: "First Response Time", name: "First Response Time",
values: result.map(d => d[1]) values: result.map(d => d.first_response_time)
}] }]
}, },
type: "line", type: "line",
@@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
hide_days: 0, hide_days: 0,
hide_seconds: 0 hide_seconds: 0
}; };
value = frappe.utils.get_formatted_duration(d, duration_options); return frappe.utils.get_formatted_duration(d, duration_options);
return value;
} }
} }
} }

View File

@@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program):
student = get_current_student() student = get_current_student()
course_enrollment = get_enrollment("course", course, student.name) course_enrollment = get_enrollment("course", course, student.name)
if not course_enrollment: if not course_enrollment:
program_enrollment = get_enrollment('program', program, student.name) program_enrollment = get_enrollment('program', program.name, student.name)
if not program_enrollment: if not program_enrollment:
frappe.throw(_("You are not enrolled in program {0}").format(program)) frappe.throw(_("You are not enrolled in program {0}").format(program))
return return
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name)) return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
else: else:
return frappe.get_doc('Course Enrollment', course_enrollment) return frappe.get_doc('Course Enrollment', course_enrollment)

View File

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

View File

@@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) {
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{ return{
query: "erpnext.controllers.queries.employee_query" query: "erpnext.controllers.queries.employee_query"
} }
} }

View File

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

View File

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

View File

@@ -207,7 +207,7 @@
"label": "Status", "label": "Status",
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Active\nInactive\nLeft", "options": "Active\nInactive\nSuspended\nLeft",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@@ -813,7 +813,7 @@
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2021-06-12 11:31:37.730760", "modified": "2021-06-17 11:31:37.730760",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr from frappe.utils import getdate, validate_email_address, today, add_years, cstr
from frappe.model.naming import set_name_by_naming_series from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.permissions import add_user_permission, remove_user_permission, \
@@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass
@@ -37,7 +36,7 @@ class Employee(NestedSet):
def validate(self): def validate(self):
from erpnext.controllers.status_updater import validate_status from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Active", "Inactive", "Left"]) validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
self.employee = self.name self.employee = self.name
self.set_employee_name() self.set_employee_name()

View File

@@ -7,7 +7,8 @@ def get_data():
'heatmap_message': _('This is based on the attendance of this Employee'), 'heatmap_message': _('This is based on the attendance of this Employee'),
'fieldname': 'employee', 'fieldname': 'employee',
'non_standard_fieldnames': { 'non_standard_fieldnames': {
'Bank Account': 'party' 'Bank Account': 'party',
'Employee Grievance': 'raised_by'
}, },
'transactions': [ 'transactions': [
{ {
@@ -20,7 +21,7 @@ def get_data():
}, },
{ {
'label': _('Lifecycle'), 'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation'] 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
}, },
{ {
'label': _('Shift'), 'label': _('Shift'),

View File

@@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
filters: [["status","=", "Active"]], filters: [["status","=", "Active"]],
get_indicator: function(doc) { get_indicator: function(doc) {
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status];
return indicator; return indicator;
} }
}; };

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Employee Grievance', {
setup: function(frm) {
frm.set_query('grievance_against_party', function() {
return {
filters: {
name: ['in', [
'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee']
]
}
};
});
frm.set_query('associated_document_type', function() {
let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website",
"Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"];
return {
filters: {
istable: 0,
issingle: 0,
module: ["Not In", ignore_modules]
}
};
});
},
grievance_against_party: function(frm) {
let filters = {};
if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) {
filters.name = ["!=", frm.doc.raised_by];
}
frm.set_query('grievance_against', function() {
return {
filters: filters
};
});
},
});

View File

@@ -0,0 +1,261 @@
{
"actions": [],
"autoname": "HR-GRIEV-.YYYY.-.#####",
"creation": "2021-05-11 13:41:51.485295",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"subject",
"raised_by",
"employee_name",
"designation",
"column_break_3",
"date",
"status",
"reports_to",
"grievance_details_section",
"grievance_against_party",
"grievance_against",
"grievance_type",
"column_break_11",
"associated_document_type",
"associated_document",
"section_break_14",
"description",
"investigation_details_section",
"cause_of_grievance",
"resolution_details_section",
"resolved_by",
"resolution_date",
"employee_responsible",
"column_break_16",
"resolution_detail",
"amended_from"
],
"fields": [
{
"fieldname": "grievance_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Grievance Type",
"options": "Grievance Type",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date ",
"reqd": 1
},
{
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Open\nInvestigated\nResolved\nInvalid",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"reqd": 1
},
{
"fieldname": "cause_of_grievance",
"fieldtype": "Text",
"label": "Cause of Grievance",
"mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\""
},
{
"fieldname": "resolution_details_section",
"fieldtype": "Section Break",
"label": "Resolution Details"
},
{
"fieldname": "resolved_by",
"fieldtype": "Link",
"label": "Resolved By",
"mandatory_depends_on": "eval: doc.status == \"Resolved\"",
"options": "User"
},
{
"fieldname": "employee_responsible",
"fieldtype": "Link",
"label": "Employee Responsible ",
"options": "Employee"
},
{
"fieldname": "resolution_detail",
"fieldtype": "Small Text",
"label": "Resolution Details",
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "resolution_date",
"fieldtype": "Date",
"label": "Resolution Date",
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
},
{
"fieldname": "grievance_against",
"fieldtype": "Dynamic Link",
"label": "Grievance Against",
"options": "grievance_against_party",
"reqd": 1
},
{
"fieldname": "raised_by",
"fieldtype": "Link",
"label": "Raised By",
"options": "Employee",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Grievance",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "raised_by.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fetch_from": "raised_by.reports_to",
"fieldname": "reports_to",
"fieldtype": "Link",
"label": "Reports To",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "grievance_details_section",
"fieldtype": "Section Break",
"label": "Grievance Details"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break"
},
{
"fieldname": "grievance_against_party",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Grievance Against Party",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "associated_document_type",
"fieldtype": "Link",
"label": "Associated Document Type",
"options": "DocType"
},
{
"fieldname": "associated_document",
"fieldtype": "Dynamic Link",
"label": "Associated Document",
"options": "associated_document_type"
},
{
"fieldname": "investigation_details_section",
"fieldtype": "Section Break",
"label": "Investigation Details"
},
{
"fetch_from": "raised_by.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-21 12:51:01.499486",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grievance",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"search_fields": "subject,raised_by,grievance_against_party",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
"track_changes": 1
}

View File

@@ -0,0 +1,15 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, bold
from frappe.model.document import Document
class EmployeeGrievance(Document):
def on_submit(self):
if self.status not in ["Invalid", "Resolved"]:
frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
bold("Invalid"),
bold("Resolved"))
)

View File

@@ -0,0 +1,12 @@
frappe.listview_settings["Employee Grievance"] = {
has_indicator_for_draft: 1,
get_indicator: function(doc) {
var colors = {
"Open": "red",
"Investigated": "orange",
"Resolved": "green",
"Invalid": "grey"
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View File

@@ -0,0 +1,51 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
import unittest
from frappe.utils import today
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeGrievance(unittest.TestCase):
def test_create_employee_grievance(self):
create_employee_grievance()
def create_employee_grievance():
grievance_type = create_grievance_type()
emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
emp_2 = make_employee("testculprit@example.com", company="_Test Company")
grievance = frappe.new_doc("Employee Grievance")
grievance.subject = "Test Employee Grievance"
grievance.raised_by = emp_1
grievance.date = today()
grievance.grievance_type = grievance_type
grievance.grievance_against_party = "Employee"
grievance.grievance_against = emp_2
grievance.description = "test descrip"
#set cause
grievance.cause_of_grievance = "test cause"
#resolution details
grievance.resolution_date = today()
grievance.resolution_detail = "test resolution detail"
grievance.resolved_by = "test_emp_grievance_@example.com"
grievance.employee_responsible = emp_2
grievance.status = "Resolved"
grievance.save()
grievance.submit()
return grievance
def create_grievance_type():
if frappe.db.exists("Grievance Type", "Employee Abuse"):
return frappe.get_doc("Grievance Type", "Employee Abuse")
grievance_type = frappe.new_doc("Grievance Type")
grievance_type.name = "Employee Abuse"
grievance_type.description = "Test"
grievance_type.save()
return grievance_type.name

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Grievance Type', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,70 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2021-05-11 12:41:50.256071",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_5",
"description"
],
"fields": [
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-21 12:54:37.764712",
"modified_by": "Administrator",
"module": "HR",
"name": "Grievance Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class GrievanceType(Document):
pass

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestGrievanceType(unittest.TestCase):
pass

View File

@@ -2,7 +2,7 @@
// MIT License. See license.txt // MIT License. See license.txt
frappe.listview_settings['Job Applicant'] = { frappe.listview_settings['Job Applicant'] = {
add_fields: ["company", "designation", "job_applicant", "status"], add_fields: ["status"],
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status == "Accepted") { if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status]; return [__(doc.status), "green", "status,=," + doc.status];

View File

@@ -110,6 +110,7 @@
"label": "Allocation" "label": "Allocation"
}, },
{ {
"allow_on_submit": 1,
"bold": 1, "bold": 1,
"fieldname": "new_leaves_allocated", "fieldname": "new_leaves_allocated",
"fieldtype": "Float", "fieldtype": "Float",
@@ -235,7 +236,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-14 15:28:26.335104", "modified": "2021-06-03 15:28:26.335104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Allocation", "name": "Leave Allocation",
@@ -277,4 +278,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"timeline_field": "employee" "timeline_field": "employee"
} }

View File

@@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.utils import set_employee_name, get_leave_period
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class BackDatedAllocationError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass
@@ -55,6 +56,43 @@ class LeaveAllocation(Document):
if self.carry_forward: if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
def on_update_after_submit(self):
if self.has_value_changed("new_leaves_allocated"):
self.validate_against_leave_applications()
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
args = {
"leaves": leaves_to_be_added,
"from_date": self.from_date,
"to_date": self.to_date,
"is_carry_forward": 0
}
create_leave_ledger_entry(self, args, True)
def get_existing_leave_count(self):
ledger_entries = frappe.get_all("Leave Ledger Entry",
filters={
"transaction_type": "Leave Allocation",
"transaction_name": self.name,
"employee": self.employee,
"company": self.company,
"leave_type": self.leave_type
},
pluck="leaves")
total_existing_leaves = 0
for entry in ledger_entries:
total_existing_leaves += entry
return total_existing_leaves
def validate_against_leave_applications(self):
leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
self.from_date, self.to_date)
if flt(leaves_taken) > flt(self.total_leaves_allocated):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
else:
frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
def update_leave_policy_assignments_when_no_allocations_left(self): def update_leave_policy_assignments_when_no_allocations_left(self):
allocations = frappe.db.get_list("Leave Allocation", filters = { allocations = frappe.db.get_list("Leave Allocation", filters = {
"docstatus": 1, "docstatus": 1,
@@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date):
def validate_carry_forward(leave_type): def validate_carry_forward(leave_type):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))

View File

@@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import erpnext
import unittest import unittest
from frappe.utils import nowdate, add_months, getdate, add_days from frappe.utils import nowdate, add_months, getdate, add_days
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
@@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.cancel() leave_allocation.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
def test_leave_addition_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 40
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
def test_leave_subtraction_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 10
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
def test_against_leave_application_validation_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
leave_application = frappe.get_doc({
"doctype": 'Leave Application',
"employee": employee.name,
"leave_type": "_Test Leave Type",
"from_date": add_months(nowdate(), 2),
"to_date": add_months(add_days(nowdate(), 10), 2),
"company": erpnext.get_default_company() or "_Test Company",
"docstatus": 1,
"status": "Approved",
"leave_approver": 'test@example.com'
})
leave_application.submit()
leave_allocation.new_leaves_allocated = 8
leave_allocation.total_leaves_allocated = 8
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
def create_leave_allocation(**args): def create_leave_allocation(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -41,7 +41,7 @@ class StaffingPlan(Document):
detail.total_estimated_cost = 0 detail.total_estimated_cost = 0
if detail.number_of_positions > 0: if detail.number_of_positions > 0:
if detail.vacancies > 0 and detail.estimated_cost_per_position: if detail.vacancies and detail.estimated_cost_per_position:
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
self.total_estimated_budget += detail.total_estimated_cost self.total_estimated_budget += detail.total_estimated_cost
@@ -76,12 +76,12 @@ class StaffingPlan(Document):
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
for {2} as per staffing plan {3} for parent company {4}." for {2} as per staffing plan {3} for parent company {4}.").format(
.format(cint(parent_plan_details[0].vacancies), cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost, parent_plan_details[0].total_estimated_cost,
frappe.bold(staffing_plan_detail.designation), frappe.bold(staffing_plan_detail.designation),
parent_plan_details[0].name, parent_plan_details[0].name,
parent_company)), ParentCompanyError) parent_company), ParentCompanyError)
#Get vacanices already planned for all companies down the hierarchy of Parent Company #Get vacanices already planned for all companies down the hierarchy of Parent Company
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
@@ -98,14 +98,14 @@ class StaffingPlan(Document):
(flt(parent_plan_details[0].total_estimated_cost) < \ (flt(parent_plan_details[0].total_estimated_cost) < \
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
.format(cint(all_sibling_details.vacancies), cint(all_sibling_details.vacancies),
all_sibling_details.total_estimated_cost, all_sibling_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation), frappe.bold(staffing_plan_detail.designation),
parent_company, parent_company,
cint(parent_plan_details[0].vacancies), cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost, parent_plan_details[0].total_estimated_cost,
parent_plan_details[0].name))) parent_plan_details[0].name))
def validate_with_subsidiary_plans(self, staffing_plan_detail): def validate_with_subsidiary_plans(self, staffing_plan_detail):
#Valdate this plan with all child company plan #Valdate this plan with all child company plan
@@ -121,11 +121,11 @@ class StaffingPlan(Document):
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
.format(self.company, self.company,
cint(children_details.vacancies), cint(children_details.vacancies),
children_details.total_estimated_cost, children_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError) frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
@frappe.whitelist() @frappe.whitelist()
def get_designation_counts(designation, company): def get_designation_counts(designation, company):
@@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
designation, from_date, to_date) designation, from_date, to_date)
# Only a single staffing plan can be active for a designation on given date # Only a single staffing plan can be active for a designation on given date
return staffing_plan if staffing_plan else None return staffing_plan if staffing_plan else None

View File

@@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', {
frappe.set_route("List", "Training Feedback"); frappe.set_route("List", "Training Feedback");
}); });
} }
} frm.events.set_employee_query(frm);
}); },
frappe.ui.form.on("Training Event Employee", { set_employee_query: function(frm) {
employee: function (frm) {
let emp = []; let emp = [];
for (let d in frm.doc.employees) { for (let d in frm.doc.employees) {
if (frm.doc.employees[d].employee) { if (frm.doc.employees[d].employee) {
@@ -40,3 +39,10 @@ frappe.ui.form.on("Training Event Employee", {
}); });
} }
}); });
frappe.ui.form.on("Training Event Employee", {
employee: function(frm) {
frm.events.set_employee_query(frm);
}
});

View File

@@ -19,6 +19,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Employee", "label": "Employee",
"no_copy": 1,
"options": "Employee" "options": "Employee"
}, },
{ {
@@ -68,7 +69,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-05-21 12:41:59.336237", "modified": "2021-07-02 17:20:27.630176",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Training Event Employee", "name": "Training Event Employee",

View File

@@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
is_carry_forward, is_expired is_carry_forward, is_expired
FROM `tabLeave Ledger Entry` FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1 AND leaves>0 AND docstatus=1
AND (from_date between %(from_date)s AND %(to_date)s AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s)) OR (from_date < %(from_date)s AND to_date > %(to_date)s))

View File

@@ -153,6 +153,24 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "Grievance Type",
"link_to": "Grievance Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grievance",
"link_to": "Employee Grievance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"dependencies": "Employee", "dependencies": "Employee",
"hidden": 0, "hidden": 0,
@@ -823,7 +841,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-04-26 13:36:15.413819", "modified": "2021-05-13 17:19:40.524444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR", "name": "HR",

View File

@@ -60,8 +60,9 @@ class Loan(AccountsController):
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
def check_sanctioned_amount_limit(self): def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
@@ -155,9 +156,29 @@ def update_total_amount_paid(doc):
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def get_total_loan_amount(applicant_type, applicant, company): def get_total_loan_amount(applicant_type, applicant, company):
return frappe.db.get_value('Loan', pending_amount = 0
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1}, loan_details = frappe.db.get_all("Loan",
'sum(loan_amount)') filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1,
"status": ("!=", "Closed")},
fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid",
"written_off_amount"])
interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type,
"company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)"))
for loan in loan_details:
if loan.status in ("Disbursed", "Loan Closure Requested"):
pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
elif loan.status == "Partially Disbursed":
pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
elif loan.status == "Sanctioned":
pending_amount += flt(loan.total_payment)
pending_amount += interest_amount
return pending_amount
def get_sanctioned_amount_limit(applicant_type, applicant, company): def get_sanctioned_amount_limit(applicant_type, applicant, company):
return frappe.db.get_value('Sanctioned Loan Amount', return frappe.db.get_value('Sanctioned Loan Amount',

View File

@@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase):
if not frappe.db.exists("Customer", "_Test Loan Customer"): if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') if not frappe.db.exists("Customer", "_Test Loan Customer 1"):
frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True)
self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name")
self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name")
create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
@@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase):
self.assertTrue(gl_entries1) self.assertTrue(gl_entries1)
self.assertTrue(gl_entries2) self.assertTrue(gl_entries2)
def test_sanctioned_amount_limit(self):
# Clear loan docs before checking
frappe.db.sql("DELETE FROM `tabLoan` where applicant = '_Test Loan Customer 1'")
frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'")
frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'")
if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer",
"applicant": "_Test Loan Customer 1", "company": "_Test Company"}):
frappe.get_doc({
"doctype": "Sanctioned Loan Amount",
"applicant_type": "Customer",
"applicant": "_Test Loan Customer 1",
"sanctioned_amount_limit": 1500000,
"company": "_Test Company"
}).insert(ignore_permissions=True)
# Make First Loan
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
# Make second loan greater than the sanctioned amount
loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge,
do_not_save=True)
self.assertRaises(frappe.ValidationError, loan_application.save)
def test_regular_loan_repayment(self): def test_regular_loan_repayment(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
@@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase):
unpledge_request.load_from_db() unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1) self.assertEqual(unpledge_request.docstatus, 1)
def test_santined_loan_security_unpledge(self): def test_sanctioned_loan_security_unpledge(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
"qty": 4000.00 "qty": 4000.00
@@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount):
return lr return lr
def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None,
repayment_periods=None, posting_date=None): repayment_periods=None, posting_date=None, do_not_save=False):
loan_application = frappe.new_doc('Loan Application') loan_application = frappe.new_doc('Loan Application')
loan_application.applicant_type = 'Customer' loan_application.applicant_type = 'Customer'
loan_application.company = company loan_application.company = company
@@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
for pledge in proposed_pledges: for pledge in proposed_pledges:
loan_application.append('proposed_pledges', pledge) loan_application.append('proposed_pledges', pledge)
if do_not_save:
return loan_application
loan_application.save() loan_application.save()
loan_application.submit() loan_application.submit()

View File

@@ -46,9 +46,11 @@ class LoanApplication(Document):
frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount))
def check_sanctioned_amount_limit(self): def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))

View File

@@ -235,70 +235,71 @@ class LoanRepayment(AccountsController):
else: else:
remarks = _("Repayment against Loan: ") + self.against_loan remarks = _("Repayment against Loan: ") + self.against_loan
if self.total_penalty_paid: if not loan_details.repay_from_salary:
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.penalty_income_account,
"against": loan_details.payment_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": loan_details.payment_account,
"against": loan_details.payment_account, "against": loan_details.loan_account + ", " + loan_details.interest_income_account
"debit": self.total_penalty_paid, + ", " + loan_details.penalty_income_account,
"debit_in_account_currency": self.total_penalty_paid, "debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
) )
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.penalty_income_account, "account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": loan_details.payment_account, "against": loan_details.payment_account,
"credit": self.total_penalty_paid, "credit": self.amount_paid,
"credit_in_account_currency": self.total_penalty_paid, "credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
) )
gle_map.append( if gle_map:
self.get_gl_dict({ make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
"account": loan_details.payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": loan_details.payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
def create_repayment_entry(loan, applicant, company, posting_date, loan_type, def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):

View File

@@ -35,7 +35,9 @@
"no_copy": 1, "no_copy": 1,
"options": "Loan Security Pledge", "options": "Loan Security Pledge",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "loan_application.applicant", "fetch_from": "loan_application.applicant",
@@ -45,47 +47,63 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Applicant", "label": "Applicant",
"options": "applicant_type", "options": "applicant_type",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "loan_security_details_section", "fieldname": "loan_security_details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Loan Security Details" "label": "Loan Security Details",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "loan", "fieldname": "loan",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Loan", "label": "Loan",
"options": "Loan" "options": "Loan",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "loan_application", "fieldname": "loan_application",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Loan Application", "label": "Loan Application",
"options": "Loan Application" "options": "Loan Application",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "total_security_value", "fieldname": "total_security_value",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Security Value", "label": "Total Security Value",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "maximum_loan_value", "fieldname": "maximum_loan_value",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Maximum Loan Value", "label": "Maximum Loan Value",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "loan_details_section", "fieldname": "loan_details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Loan Details" "label": "Loan Details",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "Requested", "default": "Requested",
@@ -94,37 +112,49 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"options": "Requested\nUnpledged\nPledged\nPartially Pledged", "options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "pledge_time", "fieldname": "pledge_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Pledge Time", "label": "Pledge Time",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "securities", "fieldname": "securities",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Securities", "label": "Securities",
"options": "Pledge", "options": "Pledge",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_10", "fieldname": "section_break_10",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Totals" "label": "Totals",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "loan.applicant_type", "fetch_from": "loan.applicant_type",
@@ -132,35 +162,45 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Applicant Type", "label": "Applicant Type",
"options": "Employee\nMember\nCustomer", "options": "Employee\nMember\nCustomer",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "more_information_section", "fieldname": "more_information_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "More Information" "label": "More Information",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "reference_no", "fieldname": "reference_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Reference No" "label": "Reference No",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Description" "label": "Description",
"show_days": 1,
"show_seconds": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 18:23:16.953305", "modified": "2021-06-29 17:15:16.082256",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Security Pledge", "name": "Loan Security Pledge",

View File

@@ -23,6 +23,12 @@ class LoanSecurityPledge(Document):
update_shortfall_status(self.loan, self.total_security_value) update_shortfall_status(self.loan, self.total_security_value)
update_loan(self.loan, self.maximum_loan_value) update_loan(self.loan, self.maximum_loan_value)
def on_cancel(self):
if self.loan:
self.db_set("status", "Cancelled")
self.db_set("pledge_time", None)
update_loan(self.loan, self.maximum_loan_value, cancel=1)
def validate_duplicate_securities(self): def validate_duplicate_securities(self):
security_list = [] security_list = []
for security in self.securities: for security in self.securities:
@@ -36,7 +42,7 @@ class LoanSecurityPledge(Document):
existing_pledge = '' existing_pledge = ''
if self.loan: if self.loan:
existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['name']) existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name'])
if existing_pledge: if existing_pledge:
loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type']) loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type'])
@@ -77,8 +83,12 @@ class LoanSecurityPledge(Document):
self.total_security_value = total_security_value self.total_security_value = total_security_value
self.maximum_loan_value = maximum_loan_value self.maximum_loan_value = maximum_loan_value
def update_loan(loan, maximum_value_against_pledge): def update_loan(loan, maximum_value_against_pledge, cancel=0):
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount']) maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 if cancel:
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s
WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan))
else:
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))

View File

@@ -13,7 +13,7 @@ frappe.ui.form.on('Blanket Order', {
refresh: function(frm) { refresh: function(frm) {
erpnext.hide_company(); erpnext.hide_company();
if (frm.doc.customer && frm.doc.docstatus === 1) { if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
frm.add_custom_button(__("Sales Order"), function() { frm.add_custom_button(__("Sales Order"), function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2018-05-24 07:18:08.256060", "creation": "2018-05-24 07:18:08.256060",
"doctype": "DocType", "doctype": "DocType",
@@ -79,6 +80,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "to_date", "fieldname": "to_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "To Date", "label": "To Date",
@@ -129,8 +131,10 @@
"label": "Terms and Conditions Details" "label": "Terms and Conditions Details"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-11-18 19:37:37.151686", "links": [],
"modified": "2021-06-29 00:30:30.621636",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Blanket Order", "name": "Blanket Order",

View File

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

View File

@@ -36,6 +36,9 @@
"materials_section", "materials_section",
"inspection_required", "inspection_required",
"quality_inspection_template", "quality_inspection_template",
"column_break_31",
"bom_level",
"section_break_33",
"items", "items",
"scrap_section", "scrap_section",
"scrap_items", "scrap_items",
@@ -193,6 +196,7 @@
}, },
{ {
"default": "Work Order", "default": "Work Order",
"depends_on": "with_operations",
"fieldname": "transfer_material_against", "fieldname": "transfer_material_against",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Transfer Material Against", "label": "Transfer Material Against",
@@ -235,6 +239,7 @@
{ {
"fieldname": "operations_section", "fieldname": "operations_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
@@ -245,6 +250,7 @@
"options": "Routing" "options": "Routing"
}, },
{ {
"depends_on": "with_operations",
"fieldname": "operations", "fieldname": "operations",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Operations", "label": "Operations",
@@ -510,6 +516,22 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_31",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "bom_level",
"fieldtype": "Int",
"label": "BOM Level",
"read_only": 1
},
{
"fieldname": "section_break_33",
"fieldtype": "Section Break",
"hide_border": 1
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
@@ -517,7 +539,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-21 12:29:32.634952", "modified": "2021-05-16 12:25:09.081968",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

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

View File

@@ -1,13 +1,31 @@
<div style="padding: 15px;"> <div style="padding: 15px;">
{% if data.image %} <div class="row mb-5">
<img class="responsive" src={{ data.image }}> <div class="col-md-5" style="max-height: 500px">
<hr style="margin: 15px -15px;"> {% if data.image %}
{% endif %} <div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
<h4> <img class="responsive" src={{ data.image }}>
{{ __("Description") }} </div>
</h4> {% endif %}
<div style="padding-top: 10px;"> </div>
{{ data.description }} <div class="col-md-7 h-500">
<h4>
{{ __("Description") }}
</h4>
<div style="padding-top: 10px;">
{{ data.description }}
</div>
<hr style="margin: 15px -15px;">
<p>
{% if data.value %}
<a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
{{ __("Open BOM {0}", [data.value.bold()]) }}</a>
{% endif %}
{% if data.item_code %}
<a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
{% endif %}
</p>
</div>
</div> </div>
<hr style="margin: 15px -15px;"> <hr style="margin: 15px -15px;">
<p> <p>

View File

@@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = {
if(node.is_root && node.data.value!="BOM") { if(node.is_root && node.data.value!="BOM") {
frappe.model.with_doc("BOM", node.data.value, function() { frappe.model.with_doc("BOM", node.data.value, function() {
var bom = frappe.model.get_doc("BOM", node.data.value); var bom = frappe.model.get_doc("BOM", node.data.value);
node.data.image = bom.image || ""; node.data.image = escape(bom.image) || "";
node.data.description = bom.description || ""; node.data.description = bom.description || "";
}); });
} }

View File

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

View File

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

View File

@@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', {
} }
}; };
}); });
frm.set_indicator_formatter('sub_operation',
function(doc) {
if (doc.status == "Pending") {
return "red";
} else {
return doc.status === "Complete" ? "green" : "orange";
}
}
);
}, },
refresh: function(frm) { refresh: function(frm) {
@@ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', {
} }
} }
if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
frm.trigger('setup_corrective_job_card');
}
frm.set_query("quality_inspection", function() { frm.set_query("quality_inspection", function() {
return { return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
@@ -43,12 +57,62 @@ frappe.ui.form.on('Job Card', {
frm.trigger("toggle_operation_number"); frm.trigger("toggle_operation_number");
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) if (frm.doc.docstatus == 0 && !frm.is_new() &&
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons"); frm.trigger("prepare_timer_buttons");
} }
}, },
setup_corrective_job_card: function(frm) {
frm.add_custom_button(__('Corrective Job Card'), () => {
let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
let fields = [
{
fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
fieldname: 'operation', get_query() {
return {
filters: {
"is_corrective_operation": 1
}
};
}
}, {
fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
fieldname: 'for_operation', get_query() {
return {
filters: {
"name": ["in", operations]
}
};
}
}
];
frappe.prompt(fields, d => {
frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
}, __("Select Corrective Operation"));
}, __('Make'));
},
make_corrective_job_card: function(frm, operation, for_operation) {
frappe.call({
method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card',
args: {
source_name: frm.doc.name,
operation: operation,
for_operation: for_operation
},
callback: function(r) {
if (r.message) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
});
},
operation: function(frm) { operation: function(frm) {
frm.trigger("toggle_operation_number"); frm.trigger("toggle_operation_number");
@@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
prepare_timer_buttons: function(frm) { prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard"); frm.trigger("make_dashboard");
if (!frm.doc.job_started) {
frm.add_custom_button(__("Start"), () => { if (!frm.doc.started_time && !frm.doc.current_time) {
if (!frm.doc.employee) { frm.add_custom_button(__("Start Job"), () => {
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
fieldname: 'employee'}, d => { frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
if (d.employee) { options: "Job Card Time Log", fieldname: 'employees'}, d => {
frm.set_value("employee", d.employee); frm.events.start_job(frm, "Work In Progress", d.employees);
} else { }, __("Assign Job to Employee"));
frm.events.start_job(frm);
}
}, __("Enter Value"), __("Start"));
} else { } else {
frm.events.start_job(frm); frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
} }
}).addClass("btn-primary"); }).addClass("btn-primary");
} else if (frm.doc.status == "On Hold") { } else if (frm.doc.status == "On Hold") {
frm.add_custom_button(__("Resume"), () => { frm.add_custom_button(__("Resume Job"), () => {
frappe.flags.resume_job = 1; frm.events.start_job(frm, "Resume Job", frm.doc.employee);
frm.events.start_job(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
} else { } else {
frm.add_custom_button(__("Pause"), () => { frm.add_custom_button(__("Pause Job"), () => {
frappe.flags.pause_job = 1; frm.events.complete_job(frm, "On Hold");
frm.set_value("status", "On Hold");
frm.events.complete_job(frm);
}); });
frm.add_custom_button(__("Complete"), () => { frm.add_custom_button(__("Complete Job"), () => {
let completed_time = frappe.datetime.now_datetime(); var sub_operations = frm.doc.sub_operations;
frm.trigger("hide_timer");
if (frm.doc.for_quantity) { let set_qty = true;
if (sub_operations && sub_operations.length > 1) {
set_qty = false;
let last_op_row = sub_operations[sub_operations.length - 2];
if (last_op_row.status == 'Complete') {
set_qty = true;
}
}
if (set_qty) {
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { fieldname: 'qty', default: frm.doc.for_quantity}, data => {
frm.events.complete_job(frm, completed_time, data.qty); frm.events.complete_job(frm, "Complete", data.qty);
}, __("Enter Value"), __("Complete")); }, __("Enter Value"));
} else { } else {
frm.events.complete_job(frm, completed_time, 0); frm.events.complete_job(frm, "Complete", 0.0);
} }
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
}, },
start_job: function(frm) { start_job: function(frm, status, employee) {
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); const args = {
row.from_time = frappe.datetime.now_datetime(); job_card_id: frm.doc.name,
frm.set_value('job_started', 1); start_time: frappe.datetime.now_datetime(),
frm.set_value('started_time' , row.from_time); employees: employee,
frm.set_value("status", "Work In Progress"); status: status
};
if (!frappe.flags.resume_job) { frm.events.make_time_log(frm, args);
frm.set_value('current_time' , 0);
}
frm.save();
}, },
complete_job: function(frm, completed_time, completed_qty) { complete_job: function(frm, status, completed_qty) {
frm.doc.time_logs.forEach(d => { const args = {
if (d.from_time && !d.to_time) { job_card_id: frm.doc.name,
d.to_time = completed_time || frappe.datetime.now_datetime(); complete_time: frappe.datetime.now_datetime(),
d.completed_qty = completed_qty || 0; status: status,
completed_qty: completed_qty
};
frm.events.make_time_log(frm, args);
},
if(frappe.flags.pause_job) { make_time_log: function(frm, args) {
let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; frm.events.update_sub_operation(frm, args);
frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0));
} else {
frm.set_value('started_time' , '');
frm.set_value('job_started', 0);
frm.set_value('current_time' , 0);
}
frm.save(); frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
args: {
args: args
},
freeze: true,
callback: function () {
frm.reload_doc();
frm.trigger("make_dashboard");
} }
}); });
}, },
update_sub_operation: function(frm, args) {
if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete');
if (sub_operations && sub_operations.length) {
args["sub_operation"] = sub_operations[0].sub_operation;
}
}
},
validate: function(frm) { validate: function(frm) {
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) { if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
frm.trigger("reset_timer"); frm.trigger("reset_timer");
} }
}, },
employee: function(frm) {
if (frm.doc.job_started && !frm.doc.current_time) {
frm.trigger("reset_timer");
} else {
frm.events.start_job(frm);
}
},
reset_timer: function(frm) { reset_timer: function(frm) {
frm.set_value('started_time' , ''); frm.set_value('started_time' , '');
frm.set_value('job_started', 0);
frm.set_value('current_time' , 0);
}, },
make_dashboard: function(frm) { make_dashboard: function(frm) {
@@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
}, },
to_time: function(frm) { to_time: function(frm) {
frm.set_value('job_started', 0);
frm.set_value('started_time', ''); frm.set_value('started_time', '');
} }
}) })

View File

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

View File

@@ -5,11 +5,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import datetime import datetime
import json
from frappe import _, bold from frappe import _, bold
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form) get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
@@ -25,10 +26,21 @@ class JobCard(Document):
self.set_status() self.set_status()
self.validate_operation_id() self.validate_operation_id()
self.validate_sequence_id() self.validate_sequence_id()
self.get_sub_operations()
self.update_sub_operation_status()
def get_sub_operations(self):
if self.operation:
self.sub_operations = []
for row in frappe.get_all("Sub Operation",
filters = {"parent": self.operation}, fields=["operation", "idx"]):
row.status = "Pending"
row.sub_operation = row.operation
self.append("sub_operations", row)
def validate_time_logs(self): def validate_time_logs(self):
self.total_completed_qty = 0.0
self.total_time_in_mins = 0.0 self.total_time_in_mins = 0.0
self.total_completed_qty = 0.0
if self.get('time_logs'): if self.get('time_logs'):
for d in self.get('time_logs'): for d in self.get('time_logs'):
@@ -44,11 +56,14 @@ class JobCard(Document):
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
self.total_time_in_mins += d.time_in_mins self.total_time_in_mins += d.time_in_mins
if d.completed_qty: if d.completed_qty and not self.sub_operations:
self.total_completed_qty += d.completed_qty self.total_completed_qty += d.completed_qty
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
for row in self.sub_operations:
self.total_completed_qty += row.completed_qty
def get_overlap_for(self, args, check_next_available_slot=False): def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1 production_capacity = 1
@@ -57,7 +72,7 @@ class JobCard(Document):
self.workstation, 'production_capacity') or 1 self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s " validate_overlap_for = " and jc.workstation = %(workstation)s "
if self.employee: if args.get("employee"):
# override capacity for employee # override capacity for employee
production_capacity = 1 production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s " validate_overlap_for = " and jc.employee = %(employee)s "
@@ -80,7 +95,7 @@ class JobCard(Document):
"to_time": args.to_time, "to_time": args.to_time,
"name": args.name or "No Name", "name": args.name or "No Name",
"parent": args.parent or "No Name", "parent": args.parent or "No Name",
"employee": self.employee, "employee": args.get("employee"),
"workstation": self.workstation "workstation": self.workstation
}, as_dict=True) }, as_dict=True)
@@ -158,6 +173,108 @@ class JobCard(Document):
row.planned_start_time = datetime.datetime.combine(start_date, row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time)) get_time(workstation_doc.working_hours[0].start_time))
def add_time_log(self, args):
last_row = []
employees = args.employees
if isinstance(employees, str):
employees = json.loads(employees)
if self.time_logs and len(self.time_logs) > 0:
last_row = self.time_logs[-1]
self.reset_timer_value(args)
if last_row and args.get("complete_time"):
for row in self.time_logs:
if not row.to_time:
row.update({
"to_time": get_datetime(args.get("complete_time")),
"operation": args.get("sub_operation"),
"completed_qty": args.get("completed_qty") or 0.0
})
elif args.get("start_time"):
new_args = {
"from_time": get_datetime(args.get("start_time")),
"operation": args.get("sub_operation"),
"completed_qty": 0.0
}
if employees:
for name in employees:
new_args.employee = name.get('employee')
self.add_start_time_log(new_args)
else:
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):
self.append("time_logs", args)
def set_employees(self, employees):
for name in employees:
self.append('employee', {
'employee': name.get('employee'),
'completed_qty': 0.0
})
def reset_timer_value(self, args):
self.started_time = None
if args.get("status") in ["Work In Progress", "Complete"]:
self.current_time = 0.0
if args.get("status") == "Work In Progress":
self.started_time = get_datetime(args.get("start_time"))
if args.get("status") == "Resume Job":
args["status"] = "Work In Progress"
if args.get("status"):
self.status = args.get("status")
def update_sub_operation_status(self):
if not (self.sub_operations and self.time_logs):
return
operation_wise_completed_time = {}
for time_log in self.time_logs:
if time_log.operation not in operation_wise_completed_time:
operation_wise_completed_time.setdefault(time_log.operation,
frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
op_row = operation_wise_completed_time[time_log.operation]
op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
if self.status == 'On Hold':
op_row.status = 'Pause'
op_row.employee.append(time_log.employee)
if time_log.time_in_mins:
op_row.completed_time += time_log.time_in_mins
op_row.completed_qty += time_log.completed_qty
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
if operation_deatils:
if row.status != 'Complete':
row.status = operation_deatils.status
row.completed_time = operation_deatils.completed_time
if operation_deatils.employee:
row.completed_time = row.completed_time / len(set(operation_deatils.employee))
if operation_deatils.completed_qty:
row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
else:
row.status = 'Pending'
row.completed_time = 0.0
row.completed_qty = 0.0
def update_time_logs(self, row): def update_time_logs(self, row):
self.append("time_logs", { self.append("time_logs", {
"from_time": row.planned_start_time, "from_time": row.planned_start_time,
@@ -182,15 +299,18 @@ class JobCard(Document):
if self.get('operation') == d.operation: if self.get('operation') == d.operation:
self.append('items', { self.append('items', {
'item_code': d.item_code, "item_code": d.item_code,
'source_warehouse': d.source_warehouse, "source_warehouse": d.source_warehouse,
'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'), "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
'item_name': d.item_name, "item_name": d.item_name,
'description': d.description, "description": d.description,
'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
"rate": d.rate,
"amount": d.amount
}) })
def on_submit(self): def on_submit(self):
self.validate_transfer_qty()
self.validate_job_card() self.validate_job_card()
self.update_work_order() self.update_work_order()
self.set_transferred_qty() self.set_transferred_qty()
@@ -199,7 +319,16 @@ class JobCard(Document):
self.update_work_order() self.update_work_order()
self.set_transferred_qty() self.set_transferred_qty()
def validate_transfer_qty(self):
if self.items and self.transferred_qty < self.for_quantity:
frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
.format(self.name))
def validate_job_card(self): def validate_job_card(self):
if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
.format(get_link_to_form('Work Order', self.work_order)))
if not self.time_logs: if not self.time_logs:
frappe.throw(_("Time logs are required for {0} {1}") frappe.throw(_("Time logs are required for {0} {1}")
.format(bold("Job Card"), get_link_to_form("Job Card", self.name))) .format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
@@ -215,6 +344,10 @@ class JobCard(Document):
if not self.work_order: if not self.work_order:
return return
if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
'add_corrective_operation_cost_in_finished_good_valuation')):
return
for_quantity, time_in_mins = 0, 0 for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], [] from_time_list, to_time_list = [], []
@@ -225,10 +358,24 @@ class JobCard(Document):
time_in_mins = flt(data[0].time_in_mins) time_in_mins = flt(data[0].time_in_mins)
wo = frappe.get_doc('Work Order', self.work_order) wo = frappe.get_doc('Work Order', self.work_order)
if self.operation_id:
if self.is_corrective_job_card:
self.update_corrective_in_work_order(wo)
elif self.operation_id:
self.validate_produced_quantity(for_quantity, wo) self.validate_produced_quantity(for_quantity, wo)
self.update_work_order_data(for_quantity, time_in_mins, wo) self.update_work_order_data(for_quantity, time_in_mins, wo)
def update_corrective_in_work_order(self, wo):
wo.corrective_operation_cost = 0.0
for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
wo.calculate_operating_cost()
wo.flags.ignore_validate_update_after_submit = True
wo.save()
def validate_produced_quantity(self, for_quantity, wo): def validate_produced_quantity(self, for_quantity, wo):
if self.docstatus < 2: return if self.docstatus < 2: return
@@ -248,8 +395,8 @@ class JobCard(Document):
min(from_time) as start_time, max(to_time) as end_time min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE WHERE
jctl.parent = jc.name and jc.work_order = %s jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
and jc.operation_id = %s and jc.docstatus = 1 and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
""", (self.work_order, self.operation_id), as_dict=1) """, (self.work_order, self.operation_id), as_dict=1)
for data in wo.operations: for data in wo.operations:
@@ -271,7 +418,8 @@ class JobCard(Document):
def get_current_operation_data(self): def get_current_operation_data(self):
return frappe.get_all('Job Card', return frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
"is_corrective_job_card": 0})
def set_transferred_qty_in_job_card(self, ste_doc): def set_transferred_qty_in_job_card(self, ste_doc):
for row in ste_doc.items: for row in ste_doc.items:
@@ -354,7 +502,11 @@ class JobCard(Document):
.format(bold(self.operation), work_order), OperationMismatchError) .format(bold(self.operation), work_order), OperationMismatchError)
def validate_sequence_id(self): def validate_sequence_id(self):
if not (self.work_order and self.sequence_id): return if self.is_corrective_job_card:
return
if not (self.work_order and self.sequence_id):
return
current_operation_qty = 0.0 current_operation_qty = 0.0
data = self.get_current_operation_data() data = self.get_current_operation_data()
@@ -376,6 +528,17 @@ class JobCard(Document):
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
@frappe.whitelist()
def make_time_log(args):
if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
doc = frappe.get_doc("Job Card", args.job_card_id)
doc.validate_sequence_id()
doc.add_time_log(args)
@frappe.whitelist() @frappe.whitelist()
def get_operation_details(work_order, operation): def get_operation_details(work_order, operation):
if work_order and operation: if work_order and operation:
@@ -511,3 +674,28 @@ def get_job_details(start, end, filters=None):
events.append(job_card_data) events.append(job_card_data)
return events return events
@frappe.whitelist()
def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
def set_missing_values(source, target):
target.is_corrective_job_card = 1
target.operation = operation
target.for_operation = for_operation
target.set('time_logs', [])
target.set('employee', [])
target.set('items', [])
target.get_sub_operations()
target.get_required_items()
target.validate_time_logs()
doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": {
"doctype": "Job Card",
"field_map": {
"name": "for_job_card",
},
}
}, target_doc, set_missing_values)
return doclist

View File

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

View File

@@ -0,0 +1,59 @@
{
"actions": [],
"creation": "2020-12-07 16:58:38.449041",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sub_operation",
"completed_time",
"status",
"completed_qty"
],
"fields": [
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Complete\nPause\nPending\nWork In Progress",
"read_only": 1
},
{
"description": "In mins",
"fieldname": "completed_time",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Completed Time",
"read_only": 1
},
{
"fieldname": "sub_operation",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Operation",
"options": "Operation",
"read_only": 1
},
{
"fieldname": "completed_qty",
"fieldtype": "Float",
"label": "Completed Qty",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-03-16 18:24:35.399593",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Operation",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class JobCardOperation(Document):
pass

View File

@@ -1,14 +1,17 @@
{ {
"actions": [],
"creation": "2019-03-08 23:56:43.187569", "creation": "2019-03-08 23:56:43.187569",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"employee",
"from_time", "from_time",
"to_time", "to_time",
"column_break_2", "column_break_2",
"time_in_mins", "time_in_mins",
"completed_qty" "completed_qty",
"operation"
], ],
"fields": [ "fields": [
{ {
@@ -41,10 +44,27 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Completed Qty", "label": "Completed Qty",
"reqd": 1 "reqd": 1
},
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
"options": "Employee"
},
{
"fieldname": "operation",
"fieldtype": "Link",
"label": "Operation",
"no_copy": 1,
"options": "Operation",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"modified": "2019-12-03 12:56:02.285448", "links": [],
"modified": "2020-12-23 14:30:00.970916",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Time Log", "name": "Job Card Time Log",

View File

@@ -26,7 +26,10 @@
"column_break_16", "column_break_16",
"overproduction_percentage_for_work_order", "overproduction_percentage_for_work_order",
"other_settings_section", "other_settings_section",
"update_bom_costs_automatically" "update_bom_costs_automatically",
"add_corrective_operation_cost_in_finished_good_valuation",
"column_break_23",
"make_serial_no_batch_from_work_order"
], ],
"fields": [ "fields": [
{ {
@@ -155,13 +158,30 @@
{ {
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "column_break_23",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order",
"fieldname": "make_serial_no_batch_from_work_order",
"fieldtype": "Check",
"label": "Make Serial No / Batch from Work Order"
},
{
"default": "0",
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
"fieldtype": "Check",
"label": "Add Corrective Operation Cost in Finished Good Valuation"
} }
], ],
"icon": "icon-wrench", "icon": "icon-wrench",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-10-13 10:55:43.996581", "modified": "2021-03-16 15:54:38.967341",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing Settings", "name": "Manufacturing Settings",
@@ -178,4 +198,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -2,7 +2,13 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Operation', { frappe.ui.form.on('Operation', {
refresh: function(frm) { setup: function(frm) {
frm.set_query('operation', 'sub_operations', function() {
return {
filters: {
'name': ['not in', [frm.doc.name]]
}
};
});
} }
}); });

View File

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

View File

@@ -2,9 +2,34 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
class Operation(Document): class Operation(Document):
def validate(self): def validate(self):
if not self.description: if not self.description:
self.description = self.name self.description = self.name
self.duplicate_sub_operation()
self.set_total_time()
def duplicate_sub_operation(self):
operation_list = []
for row in self.sub_operations:
if row.operation in operation_list:
frappe.throw(_("The operation {0} can not add multiple times")
.format(frappe.bold(row.operation)))
if self.name == row.operation:
frappe.throw(_("The operation {0} can not be the sub operation")
.format(frappe.bold(row.operation)))
operation_list.append(row.operation)
def set_total_time(self):
self.total_operation_time = 0.0
for row in self.sub_operations:
self.total_operation_time += row.time_in_mins

View File

@@ -4,7 +4,7 @@
frappe.ui.form.on('Production Plan', { frappe.ui.form.on('Production Plan', {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Work Order': 'Work Order', 'Work Order': 'Work Order / Subcontract PO',
'Material Request': 'Material Request', 'Material Request': 'Material Request',
}; };
@@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', {
frm.trigger("show_progress"); frm.trigger("show_progress");
if (frm.doc.status !== "Completed") { if (frm.doc.status !== "Completed") {
if (frm.doc.po_items && frm.doc.status !== "Closed") { frm.add_custom_button(__("Work Order Tree"), ()=> {
frm.add_custom_button(__("Work Order"), ()=> { frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name});
frm.trigger("make_work_order"); }, __('View'));
}, __('Create'));
}
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { frm.add_custom_button(__("Production Plan Summary"), ()=> {
frm.add_custom_button(__("Material Request"), ()=> { frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name});
frm.trigger("make_material_request"); }, __('View'));
}, __('Create'));
}
if (frm.doc.status === "Closed") { if (frm.doc.status === "Closed") {
frm.add_custom_button(__("Re-open"), function() { frm.add_custom_button(__("Re-open"), function() {
@@ -89,6 +85,18 @@ frappe.ui.form.on('Production Plan', {
frm.events.close_open_production_plan(frm, true); frm.events.close_open_production_plan(frm, true);
}, __("Status")); }, __("Status"));
} }
if (frm.doc.po_items && frm.doc.status !== "Closed") {
frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> {
frm.trigger("make_work_order");
}, __('Create'));
}
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
frm.add_custom_button(__("Material Request"), ()=> {
frm.trigger("make_material_request");
}, __('Create'));
}
} }
} }
@@ -233,6 +241,17 @@ frappe.ui.form.on('Production Plan', {
}); });
}, },
get_sub_assembly_items: function(frm) {
frappe.call({
method: "get_sub_assembly_items",
freeze: true,
doc: frm.doc,
callback: function() {
refresh_field("sub_assembly_items");
}
});
},
get_items_for_mr: function(frm) { get_items_for_mr: function(frm) {
if (!frm.doc.for_warehouse) { if (!frm.doc.for_warehouse) {
frappe.throw(__("Select warehouse for material requests")); frappe.throw(__("Select warehouse for material requests"));
@@ -306,8 +325,25 @@ frappe.ui.form.on('Production Plan', {
}, },
download_materials_required: function(frm) { download_materials_required: function(frm) {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; const fields = [{
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); fieldname: 'warehouses',
fieldtype: 'Table MultiSelect',
label: __('Warehouses'),
default: frm.doc.from_warehouse,
options: "Production Plan Material Request Warehouse",
get_query: function () {
return {
filters: {
company: frm.doc.company
}
};
},
}];
frappe.prompt(fields, (row) => {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses });
}, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock'));
}, },
show_progress: function(frm) { show_progress: function(frm) {

Some files were not shown because too many files have changed in this diff Show More