diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 2610c8655ef..9dff1168fde 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -37,7 +37,7 @@ class Account(NestedSet): def autoname(self): from erpnext.accounts.utils import get_autoname_with_number - self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) + self.name = get_autoname_with_number(self.account_number, self.account_name, self.company) def validate(self): from erpnext.accounts.utils import validate_field_number diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 31055c3fb42..e8b34bbf034 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -16,7 +16,7 @@ class CostCenter(NestedSet): from erpnext.accounts.utils import get_autoname_with_number self.name = get_autoname_with_number( - self.cost_center_number, self.cost_center_name, None, self.company + self.cost_center_number, self.cost_center_name, self.company ) def validate(self): diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 63c6547f1d4..52690e1e662 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -184,7 +184,9 @@ class JournalEntry(AccountsController): } ) - tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category) + tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details( + inv, self.tax_withholding_category + ) if not tax_withholding_details: return diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 0f530794030..6039bdfe95f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1091,7 +1091,7 @@ frappe.ui.form.on('Payment Entry', { $.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; }); - frm.doc.paid_amount_after_tax = frm.doc.paid_amount; + frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount; }); }, @@ -1182,7 +1182,7 @@ frappe.ui.form.on('Payment Entry', { } cumulated_tax_fraction += tax.tax_fraction_for_current_item; - frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction)) + frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction)) }); }, @@ -1214,6 +1214,7 @@ frappe.ui.form.on('Payment Entry', { frm.doc.total_taxes_and_charges = 0.0; frm.doc.base_total_taxes_and_charges = 0.0; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let actual_tax_dict = {}; // maintain actual tax rate based on idx @@ -1234,8 +1235,8 @@ frappe.ui.form.on('Payment Entry', { } } - tax.tax_amount = current_tax_amount; - tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate; + // tax accounts are only in company currency + tax.base_tax_amount = current_tax_amount; current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; if(i==0) { @@ -1244,9 +1245,29 @@ frappe.ui.form.on('Payment Entry', { tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax)); } - tax.base_total = tax.total * frm.doc.source_exchange_rate; - frm.doc.total_taxes_and_charges += current_tax_amount; - frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate; + // tac accounts are only in company currency + tax.base_total = tax.total + + // calculate total taxes and base total taxes + if(frm.doc.payment_type == "Pay") { + // tax accounts only have company currency + if(tax.currency != frm.doc.paid_to_account_currency) { + //total_taxes_and_charges has the target currency. so using target conversion rate + frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate); + + } else { + frm.doc.total_taxes_and_charges += current_tax_amount; + } + } else if(frm.doc.payment_type == "Receive") { + if(tax.currency != frm.doc.paid_from_account_currency) { + //total_taxes_and_charges has the target currency. so using source conversion rate + frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate); + } else { + frm.doc.total_taxes_and_charges += current_tax_amount; + } + } + + frm.doc.base_total_taxes_and_charges += tax.base_tax_amount; frm.refresh_field('taxes'); frm.refresh_field('total_taxes_and_charges'); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 4618d0807cb..7f245fd083a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -940,6 +940,13 @@ class PaymentEntry(AccountsController): ) if not d.included_in_paid_amount: + if get_account_currency(payment_account) != self.company_currency: + if self.payment_type == "Receive": + exchange_rate = self.target_exchange_rate + elif self.payment_type in ["Pay", "Internal Transfer"]: + exchange_rate = self.source_exchange_rate + base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount")) + gl_entries.append( self.get_gl_dict( { @@ -1033,7 +1040,7 @@ class PaymentEntry(AccountsController): for fieldname in tax_fields: tax.set(fieldname, 0.0) - self.paid_amount_after_tax = self.paid_amount + self.paid_amount_after_tax = self.base_paid_amount def determine_exclusive_rate(self): if not any(cint(tax.included_in_paid_amount) for tax in self.get("taxes")): @@ -1052,7 +1059,7 @@ class PaymentEntry(AccountsController): cumulated_tax_fraction += tax.tax_fraction_for_current_item - self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction)) + self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction)) def calculate_taxes(self): self.total_taxes_and_charges = 0.0 @@ -1075,7 +1082,7 @@ class PaymentEntry(AccountsController): current_tax_amount += actual_tax_dict[tax.idx] tax.tax_amount = current_tax_amount - tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate + tax.base_tax_amount = current_tax_amount if tax.add_deduct_tax == "Deduct": current_tax_amount *= -1.0 @@ -1089,14 +1096,20 @@ class PaymentEntry(AccountsController): self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax) ) - tax.base_total = tax.total * self.source_exchange_rate + tax.base_total = tax.total if self.payment_type == "Pay": - self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) - self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) - else: - self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) - self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + if tax.currency != self.paid_to_account_currency: + self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) + else: + self.total_taxes_and_charges += current_tax_amount + elif self.payment_type == "Receive": + if tax.currency != self.paid_from_account_currency: + self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + else: + self.total_taxes_and_charges += current_tax_amount + + self.base_total_taxes_and_charges += tax.base_tax_amount if self.get("taxes"): self.paid_amount_after_tax = self.get("taxes")[-1].base_total diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 02627eb0074..123b5dfd512 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe import qb from frappe.tests.utils import FrappeTestCase from frappe.utils import flt, nowdate @@ -722,6 +723,46 @@ class TestPaymentEntry(FrappeTestCase): flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2) ) + def test_gl_of_multi_currency_payment_with_taxes(self): + payment_entry = create_payment_entry( + party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True + ) + payment_entry.append( + "taxes", + { + "account_head": "_Test Account Service Tax - _TC", + "charge_type": "Actual", + "tax_amount": 100, + "add_deduct_tax": "Add", + "description": "Test", + }, + ) + payment_entry.target_exchange_rate = 80 + payment_entry.received_amount = 12.5 + payment_entry = payment_entry.submit() + gle = qb.DocType("GL Entry") + gl_entries = ( + qb.from_(gle) + .select( + gle.account, + gle.debit, + gle.credit, + gle.debit_in_account_currency, + gle.credit_in_account_currency, + ) + .orderby(gle.account) + .where(gle.voucher_no == payment_entry.name) + .run() + ) + + expected_gl_entries = ( + ("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0), + ("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0), + ("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0), + ) + + self.assertEqual(gl_entries, expected_gl_entries) + def test_payment_entry_against_onhold_purchase_invoice(self): pi = make_purchase_invoice() diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 601fc87a227..52efd33fefa 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -14,6 +14,7 @@ from erpnext.accounts.utils import ( QueryPaymentLedger, get_outstanding_invoices, reconcile_against_document, + update_reference_in_payment_entry, ) from erpnext.controllers.accounts_controller import get_advance_payment_entries @@ -212,6 +213,23 @@ class PaymentReconciliation(Document): inv.currency = entry.get("currency") inv.outstanding_amount = flt(entry.get("outstanding_amount")) + def get_difference_amount(self, allocated_entry): + if allocated_entry.get("reference_type") != "Payment Entry": + return + + dr_or_cr = ( + "credit_in_account_currency" + if erpnext.get_party_account_type(self.party_type) == "Receivable" + else "debit_in_account_currency" + ) + + row = self.get_payment_details(allocated_entry, dr_or_cr) + + doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name) + update_reference_in_payment_entry(row, doc, do_not_save=True) + + return doc.difference_amount + @frappe.whitelist() def allocate_entries(self, args): self.validate_entries() @@ -227,12 +245,16 @@ class PaymentReconciliation(Document): res = self.get_allocated_entry(pay, inv, pay["amount"]) inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount")) pay["amount"] = 0 + + res.difference_amount = self.get_difference_amount(res) + if pay.get("amount") == 0: entries.append(res) break elif inv.get("outstanding_amount") == 0: entries.append(res) continue + else: break diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index 6ed7a3154e5..dde9980ce53 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -39,6 +39,7 @@ { "columns": 2, "fetch_from": "payment_term.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Small Text", "in_list_view": 1, @@ -159,7 +160,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-28 05:41:35.084233", + "modified": "2022-09-16 13:57:06.382859", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Schedule", @@ -168,5 +169,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 99c5b34fa35..6e7ebd1414d 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -176,7 +176,7 @@ }, { "collapsible": 1, - "depends_on": "eval:doc.apply_on != 'Transaction'", + "depends_on": "eval:doc.apply_on != 'Transaction' && !doc.mixed_conditions", "fieldname": "section_break_18", "fieldtype": "Section Break", "label": "Discount on Other Item" @@ -297,12 +297,12 @@ { "fieldname": "min_qty", "fieldtype": "Float", - "label": "Min Qty" + "label": "Min Qty (As Per Stock UOM)" }, { "fieldname": "max_qty", "fieldtype": "Float", - "label": "Max Qty" + "label": "Max Qty (As Per Stock UOM)" }, { "fieldname": "column_break_21", @@ -481,7 +481,7 @@ "description": "System will notify to increase or decrease quantity or amount ", "fieldname": "threshold_percentage", "fieldtype": "Percent", - "label": "Threshold for Suggestion" + "label": "Threshold for Suggestion (In Percentage)" }, { "description": "Higher the number, higher the priority", @@ -583,10 +583,11 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2021-08-06 15:10:04.219321", + "modified": "2022-09-16 16:00:38.356266", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -642,5 +643,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 98e0a9b2158..9af3188e476 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -324,7 +324,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if isinstance(pricing_rule, str): pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule) - pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) + pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or [] if pricing_rule.get("suggestion"): continue @@ -337,7 +337,6 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other: item_details.update( { - "apply_rule_on_other_items": json.dumps(pricing_rule.apply_rule_on_other_items), "price_or_product_discount": pricing_rule.price_or_product_discount, "apply_rule_on": ( frappe.scrub(pricing_rule.apply_rule_on_other) @@ -347,6 +346,9 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa } ) + if pricing_rule.apply_rule_on_other_items: + item_details["apply_rule_on_other_items"] = json.dumps(pricing_rule.apply_rule_on_other_items) + if pricing_rule.coupon_code_based == 1 and args.coupon_code == None: return item_details @@ -492,7 +494,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra ) if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"): - items = get_pricing_rule_items(pricing_rule) + items = get_pricing_rule_items(pricing_rule, other_items=True) item_details.apply_on = ( frappe.scrub(pricing_rule.apply_rule_on_other) if pricing_rule.apply_rule_on_other diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 3bd0cd2e837..0a9db6b0f59 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -766,6 +766,68 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") + def test_pricing_rule_for_other_items_cond_with_amount(self): + item = make_item("Water Flask New") + other_item = make_item("Other Water Flask New") + make_item_price(item.name, "_Test Price List", 100) + make_item_price(other_item.name, "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Water Flask Rule", + "apply_on": "Item Code", + "apply_rule_on_other": "Item Code", + "price_or_product_discount": "Price", + "rate_or_discount": "Discount Percentage", + "other_item_code": other_item.name, + "items": [ + { + "item_code": item.name, + } + ], + "selling": 1, + "currency": "INR", + "min_amt": 200, + "discount_percentage": 10, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice(do_not_save=True, item_code=item.name) + si.append( + "items", + { + "item_code": other_item.name, + "item_name": other_item.item_name, + "description": other_item.description, + "stock_uom": other_item.stock_uom, + "uom": other_item.stock_uom, + "cost_center": si.items[0].cost_center, + "expense_account": si.items[0].expense_account, + "warehouse": si.items[0].warehouse, + "conversion_factor": 1, + "qty": 1, + }, + ) + si.selling_price_list = "_Test Price List" + si.save() + + self.assertEqual(si.items[0].discount_percentage, 0) + self.assertEqual(si.items[1].discount_percentage, 0) + + si.items[0].qty = 2 + si.save() + + self.assertEqual(si.items[0].discount_percentage, 0) + self.assertEqual(si.items[0].stock_qty, 2) + self.assertEqual(si.items[0].amount, 200) + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[1].discount_percentage, 10) + + si.delete() + rule.delete() + test_dependencies = ["Campaign"] diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 70926cfbd72..1f29d732ba5 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -252,12 +252,6 @@ def filter_pricing_rules(args, pricing_rules, doc=None): stock_qty = flt(args.get("stock_qty")) amount = flt(args.get("price_list_rate")) * flt(args.get("qty")) - if pricing_rules[0].apply_rule_on_other: - field = frappe.scrub(pricing_rules[0].apply_rule_on_other) - - if field and pricing_rules[0].get("other_" + field) != args.get(field): - return - pr_doc = frappe.get_cached_doc("Pricing Rule", pricing_rules[0].name) if pricing_rules[0].mixed_conditions and doc: @@ -274,7 +268,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): amount += data[1] if pricing_rules[0].apply_rule_on_other and not pricing_rules[0].mixed_conditions and doc: - pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules) or [] + pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, args) or [] else: pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, args) @@ -352,16 +346,14 @@ def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, tr if fieldname: msg = _( "If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item." - ).format( - type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description) - ) + ).format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.title)) if fieldname in ["min_amt", "max_amt"]: msg = _("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.").format( type_of_transaction, fmt_money(args.get(fieldname), currency=args.get("currency")), bold(item_code), - bold(args.rule_description), + bold(args.title), ) frappe.msgprint(msg) @@ -454,17 +446,29 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): return sum_qty, sum_amt, items -def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules): - items = get_pricing_rule_items(pr_doc) +def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item): + other_items = get_pricing_rule_items(pr_doc, other_items=True) + pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on")) + apply_on = frappe.scrub(pr_doc.get("apply_on")) + + items = [] + for d in pr_doc.get(pricing_rule_apply_on): + if apply_on == "item_group": + items.extend(get_child_item_groups(d.get(apply_on))) + else: + items.append(d.get(apply_on)) for row in doc.items: - if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items: - pricing_rules = filter_pricing_rules_for_qty_amount( - row.get("stock_qty"), row.get("amount"), pricing_rules, row - ) + if row.get(apply_on) in items: + if not row.get("qty"): + continue + + stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0) + amount = stock_qty * (row.get("price_list_rate") or row.get("rate")) + pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row) if pricing_rules and pricing_rules[0]: - pricing_rules[0].apply_rule_on_other_items = items + pricing_rules[0].apply_rule_on_other_items = other_items return pricing_rules @@ -658,21 +662,21 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values doc.append("items", args) -def get_pricing_rule_items(pr_doc): +def get_pricing_rule_items(pr_doc, other_items=False) -> list: apply_on_data = [] apply_on = frappe.scrub(pr_doc.get("apply_on")) pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on")) - for d in pr_doc.get(pricing_rule_apply_on): - if apply_on == "item_group": - apply_on_data.extend(get_child_item_groups(d.get(apply_on))) - else: - apply_on_data.append(d.get(apply_on)) - - if pr_doc.apply_rule_on_other: + if pr_doc.apply_rule_on_other and other_items: apply_on = frappe.scrub(pr_doc.apply_rule_on_other) apply_on_data.append(pr_doc.get("other_" + apply_on)) + else: + for d in pr_doc.get(pricing_rule_apply_on): + if apply_on == "item_group": + apply_on_data.extend(get_child_item_groups(d.get(apply_on))) + else: + apply_on_data.append(d.get(apply_on)) return list(set(apply_on_data)) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 534b879e783..986fc038c60 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -83,6 +83,8 @@ "section_break_51", "taxes_and_charges", "taxes", + "tax_withheld_vouchers_section", + "tax_withheld_vouchers", "sec_tax_breakup", "other_charges_calculation", "totals", @@ -512,7 +514,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1367,7 +1368,7 @@ "width": "50px" }, { - "depends_on": "eval:doc.is_subcontracted", + "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", @@ -1426,13 +1427,25 @@ "hidden": 1, "label": "Is Old Subcontracting Flow", "read_only": 1 - } + }, + { + "fieldname": "tax_withheld_vouchers_section", + "fieldtype": "Section Break", + "label": "Tax Withheld Vouchers" + }, + { + "fieldname": "tax_withheld_vouchers", + "fieldtype": "Table", + "label": "Tax Withheld Vouchers", + "options": "Tax Withheld Vouchers", + "read_only": 1 + } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:40:58.527065", + "modified": "2022-09-13 23:39:54.525037", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1492,6 +1505,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title", "track_changes": 1 diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index fea81e9c272..d1853002891 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1519,7 +1519,7 @@ class PurchaseInvoice(BuyingController): if not self.tax_withholding_category: return - tax_withholding_details, advance_taxes = get_party_tax_withholding_details( + tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details( self, self.tax_withholding_category ) @@ -1548,6 +1548,19 @@ class PurchaseInvoice(BuyingController): for d in to_remove: self.remove(d) + ## Add pending vouchers on which tax was withheld + self.set("tax_withheld_vouchers", []) + + for voucher_no, voucher_details in voucher_wise_amount.items(): + self.append( + "tax_withheld_vouchers", + { + "voucher_name": voucher_no, + "voucher_type": voucher_details.get("voucher_type"), + "taxable_amount": voucher_details.get("amount"), + }, + ) + # calculate totals again after applying TDS self.calculate_taxes_and_totals() diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0a4f25b8769..f901257ccf6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1543,6 +1543,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi.save() self.assertEqual(pi.items[0].conversion_factor, 1000) + def test_batch_expiry_for_purchase_invoice(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = self.make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_invoice( + qty=1, + item_code=item.name, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 1c9d3fbfb2d..2da515737a9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -649,7 +649,6 @@ "hide_days": 1, "hide_seconds": 1, "label": "Ignore Pricing Rule", - "no_copy": 1, "print_hide": 1 }, { @@ -2022,7 +2021,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-07-11 17:43:56.435382", + "modified": "2022-09-16 17:44:22.227332", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4207156187f..afd5a59df4e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1084,7 +1084,7 @@ class SalesInvoice(SellingController): if self.is_return: fixed_asset_gl_entries = get_gl_entries_on_asset_regain( - asset, item.base_net_amount, item.finance_book + asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") ) asset.db_set("disposal_date", None) @@ -1099,7 +1099,7 @@ class SalesInvoice(SellingController): asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( - asset, item.base_net_amount, item.finance_book + asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") ) asset.db_set("disposal_date", self.posting_date) @@ -2434,7 +2434,6 @@ def create_dunning(source_name, target_doc=None): target.closing_text = letter_text.get("closing_text") target.language = letter_text.get("language") amounts = calculate_interest_and_amount( - target.posting_date, target.outstanding_amount, target.rate_of_interest, target.dunning_fee, diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py b/erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json new file mode 100644 index 00000000000..ce8c0c37086 --- /dev/null +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2022-09-13 16:18:59.404842", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_name", + "taxable_amount" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher Name", + "options": "voucher_type" + }, + { + "fieldname": "taxable_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Taxable Amount", + "options": "Company:company:default_currency" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-09-13 23:40:41.479208", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Withheld Vouchers", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py new file mode 100644 index 00000000000..ea54c5403a8 --- /dev/null +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class TaxWithheldVouchers(Document): + pass diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 6004e2b19b2..0b5df9e0cc0 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -109,7 +109,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): ).format(tax_withholding_category, inv.company, party) ) - tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount( + tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount( party_type, parties, inv, tax_details, posting_date, pan_no ) @@ -119,7 +119,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) if inv.doctype == "Purchase Invoice": - return tax_row, tax_deducted_on_advances + return tax_row, tax_deducted_on_advances, voucher_wise_amount else: return tax_row @@ -217,7 +217,9 @@ def get_lower_deduction_certificate(tax_details, pan_no): def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None): - vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type) + vouchers, voucher_wise_amount = get_invoice_vouchers( + parties, tax_details, inv.company, party_type=party_type + ) advance_vouchers = get_advance_vouchers( parties, company=inv.company, @@ -236,6 +238,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_deducted = get_deducted_tax(taxable_vouchers, tax_details) tax_amount = 0 + if party_type == "Supplier": ldc = get_lower_deduction_certificate(tax_details, pan_no) if tax_deducted: @@ -261,12 +264,13 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if cint(tax_details.round_off_tax_amount): tax_amount = round(tax_amount) - return tax_amount, tax_deducted, tax_deducted_on_advances + return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): - dr_or_cr = "credit" if party_type == "Supplier" else "debit" doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" + voucher_wise_amount = {} + vouchers = [] filters = { "company": company, @@ -281,29 +285,40 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} ) - invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""] + invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"]) - journal_entries = frappe.db.sql( + for d in invoices_details: + vouchers.append(d.name) + voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}}) + + journal_entries_details = frappe.db.sql( """ - SELECT j.name + SELECT j.name, ja.credit - ja.debit AS amount FROM `tabJournal Entry` j, `tabJournal Entry Account` ja WHERE - j.docstatus = 1 + j.name = ja.parent + AND j.docstatus = 1 AND j.is_opening = 'No' AND j.posting_date between %s and %s - AND ja.{dr_or_cr} > 0 AND ja.party in %s - """.format( - dr_or_cr=dr_or_cr + AND j.apply_tds = 1 + AND j.tax_withholding_category = %s + """, + ( + tax_details.from_date, + tax_details.to_date, + tuple(parties), + tax_details.get("tax_withholding_category"), ), - (tax_details.from_date, tax_details.to_date, tuple(parties)), - as_list=1, + as_dict=1, ) - if journal_entries: - journal_entries = journal_entries[0] + if journal_entries_details: + for d in journal_entries_details: + vouchers.append(d.name) + voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}}) - return invoices + journal_entries + return vouchers, voucher_wise_amount def get_advance_vouchers( @@ -329,23 +344,25 @@ def get_advance_vouchers( def get_taxes_deducted_on_advances_allocated(inv, tax_details): - advances = [d.reference_name for d in inv.get("advances")] tax_info = [] - if advances: - pe = frappe.qb.DocType("Payment Entry").as_("pe") - at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + if inv.get("advances"): + advances = [d.reference_name for d in inv.get("advances")] - tax_info = ( - frappe.qb.from_(at) - .inner_join(pe) - .on(pe.name == at.parent) - .select(at.parent, at.name, at.tax_amount, at.allocated_amount) - .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category")) - .where(at.parent.isin(advances)) - .where(at.account_head == tax_details.account_head) - .run(as_dict=True) - ) + if advances: + pe = frappe.qb.DocType("Payment Entry").as_("pe") + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + tax_info = ( + frappe.qb.from_(at) + .inner_join(pe) + .on(pe.name == at.parent) + .select(at.parent, at.name, at.tax_amount, at.allocated_amount) + .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category")) + .where(at.parent.isin(advances)) + .where(at.account_head == tax_details.account_head) + .run(as_dict=True) + ) return tax_info @@ -394,11 +411,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): supp_credit_amt += supp_jv_credit_amt supp_credit_amt += inv.net_total - debit_note_amount = get_debit_note_amount( - parties, tax_details.from_date, tax_details.to_date, inv.company - ) - supp_credit_amt -= debit_note_amount - threshold = tax_details.get("threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0) @@ -515,22 +527,6 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net return tds_amount -def get_debit_note_amount(suppliers, from_date, to_date, company=None): - - filters = { - "supplier": ["in", suppliers], - "is_return": 1, - "docstatus": 1, - "posting_date": ["between", (from_date, to_date)], - } - fields = ["abs(sum(net_total)) as net_total"] - - if company: - filters["company"] = company - - return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0 - - def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): if current_amount < (certificate_limit - deducted_amount): return current_amount * rate / 100 diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 3059f8d64b8..e80fe11ab30 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -52,7 +52,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): invoices.append(pi) # delete invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_single_threshold_tds(self): @@ -88,7 +88,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi.taxes_and_charges_deducted, 1000) # delete invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_tax_withholding_category_checks(self): @@ -114,7 +114,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): # TDS should be applied only on 1000 self.assertEqual(pi1.taxes[0].tax_amount, 1000) - for d in invoices: + for d in reversed(invoices): d.cancel() def test_cumulative_threshold_tcs(self): @@ -148,8 +148,8 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(tcs_charged, 500) invoices.append(si) - # delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): d.cancel() def test_tds_calculation_on_net_total(self): @@ -182,8 +182,8 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 4000) - # delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): d.cancel() def test_multi_category_single_supplier(self): @@ -207,8 +207,50 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 250) - # delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): + d.cancel() + + def test_tax_withholding_category_voucher_display(self): + frappe.db.set_value( + "Supplier", "Test TDS Supplier6", "tax_withholding_category", "Test Multi Invoice Category" + ) + invoices = [] + + pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True) + pi.apply_tds = 1 + pi.tax_withholding_category = "Test Multi Invoice Category" + pi.save() + pi.submit() + invoices.append(pi) + + pi1 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=2000, do_not_save=True) + pi1.apply_tds = 1 + pi1.is_return = 1 + pi1.items[0].qty = -1 + pi1.tax_withholding_category = "Test Multi Invoice Category" + pi1.save() + pi1.submit() + invoices.append(pi1) + + pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True) + pi2.apply_tds = 1 + pi2.tax_withholding_category = "Test Multi Invoice Category" + pi2.save() + pi2.submit() + invoices.append(pi2) + + pi2.load_from_db() + + self.assertTrue(pi2.taxes[0].tax_amount, 1100) + + self.assertTrue(pi2.tax_withheld_vouchers[0].voucher_name == pi1.name) + self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total) + self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name) + self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total) + + # cancel invoices to avoid clashing + for d in reversed(invoices): d.cancel() @@ -308,6 +350,7 @@ def create_records(): "Test TDS Supplier3", "Test TDS Supplier4", "Test TDS Supplier5", + "Test TDS Supplier6", ]: if frappe.db.exists("Supplier", name): continue @@ -498,3 +541,22 @@ def create_tax_with_holding_category(): "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], } ).insert() + + if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"): + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Test Multi Invoice Category", + "category_name": "Test Multi Invoice Category", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 5000, + "cumulative_threshold": 10000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 9d2deea523b..449ebdcd924 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -22,8 +22,7 @@ def get_columns(): { "label": _("Payment Document Type"), "fieldname": "payment_document_type", - "fieldtype": "Link", - "options": "Doctype", + "fieldtype": "Data", "width": 130, }, { @@ -33,15 +32,15 @@ def get_columns(): "options": "payment_document_type", "width": 140, }, - {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, {"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120}, - {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100}, + {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 120}, { "label": _("Against Account"), "fieldname": "against", "fieldtype": "Link", "options": "Account", - "width": 170, + "width": 200, }, {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, ] diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index e77e828e166..82f38dacd2a 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -237,9 +237,9 @@ def get_conditions(filters): or filters.get("party") or filters.get("group_by") in ["Group by Account", "Group by Party"] ): - conditions.append("posting_date >=%(from_date)s") + conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") - conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") + conditions.append("(posting_date <=%(to_date)s)") if filters.get("project"): conditions.append("project in %(project)s") diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 9d566785416..cd5f3667071 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False): for d in data: for period in period_list: key = period if consolidated else period.key - d[key] = totals[d["account"]] d["total"] = totals[d["account"]] return data diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 6bd08ad837a..6d2cd8ed411 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type): query_filters = { "company": filters.company, "from_date": filters.from_date, + "to_date": filters.to_date, "report_type": report_type, "year_start_date": filters.year_start_date, "project": filters.project, @@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type): where company=%(company)s {additional_conditions} - and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') + and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) and account in (select name from `tabAccount` where report_type=%(report_type)s) and is_cancelled = 0 group by account""".format( diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index 5fcfdff6f1a..ee223484d47 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -104,12 +104,17 @@ def get_opening_balances(filters): where company=%(company)s and is_cancelled=0 and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') + and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) {account_filter} group by party""".format( account_filter=account_filter ), - {"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type}, + { + "company": filters.company, + "from_date": filters.from_date, + "to_date": filters.to_date, + "party_type": filters.party_type, + }, as_dict=True, ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f61e8ac960b..c5eb7d8733f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1037,7 +1037,7 @@ def update_cost_center(docname, cost_center_name, cost_center_number, company, m frappe.db.set_value("Cost Center", docname, "cost_center_name", cost_center_name.strip()) - new_name = get_autoname_with_number(cost_center_number, cost_center_name, docname, company) + new_name = get_autoname_with_number(cost_center_number, cost_center_name, company) if docname != new_name: frappe.rename_doc("Cost Center", docname, new_name, force=1, merge=merge) return new_name @@ -1060,16 +1060,14 @@ def validate_field_number(doctype_name, docname, number_value, company, field_na ) -def get_autoname_with_number(number_value, doc_title, name, company): +def get_autoname_with_number(number_value, doc_title, company): """append title with prefix as number and suffix as company's abbreviation separated by '-'""" - if name: - name_split = name.split("-") - parts = [doc_title.strip(), name_split[len(name_split) - 1].strip()] - else: - abbr = frappe.get_cached_value("Company", company, ["abbr"], as_dict=True) - parts = [doc_title.strip(), abbr.abbr] + company_abbr = frappe.get_cached_value("Company", company, "abbr") + parts = [doc_title.strip(), company_abbr] + if cstr(number_value).strip(): parts.insert(0, cstr(number_value).strip()) + return " - ".join(parts) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f414930d722..5512d4159d8 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -230,7 +230,7 @@ frappe.ui.form.on('Asset', { datasets: [{ color: 'green', values: asset_values, - formatted: asset_values.map(d => d.toFixed(2)) + formatted: asset_values.map(d => d?.toFixed(2)) }] }, type: 'line' @@ -239,8 +239,10 @@ frappe.ui.form.on('Asset', { item_code: function(frm) { - if(frm.doc.item_code) { + if(frm.doc.item_code && frm.doc.calculate_depreciation) { frm.trigger('set_finance_book'); + } else { + frm.set_value('finance_books', []); } }, @@ -381,6 +383,11 @@ frappe.ui.form.on('Asset', { calculate_depreciation: function(frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); + if (frm.doc.item_code && frm.doc.calculate_depreciation ) { + frm.trigger("set_finance_book"); + } else { + frm.set_value("finance_books", []); + } }, gross_purchase_amount: function(frm) { diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index e4765634c8d..b6e86cdd5a4 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -243,7 +243,6 @@ def restore_asset(asset_name): asset.set_status() - def depreciate_asset(asset, date): asset.flags.ignore_validate_update_after_submit = True asset.prepare_depreciation_data(date_of_disposal=date) @@ -328,8 +327,9 @@ def disposal_happens_in_the_future(posting_date_of_disposal): return False - -def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): +def get_gl_entries_on_asset_regain( + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None +): ( fixed_asset_account, asset, @@ -341,28 +341,45 @@ def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): ) = get_asset_details(asset, finance_book) gl_entries = [ - { - "account": fixed_asset_account, - "debit_in_account_currency": asset.gross_purchase_amount, - "debit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center, - }, - { - "account": accumulated_depr_account, - "credit_in_account_currency": accumulated_depr_amount, - "credit": accumulated_depr_amount, - "cost_center": depreciation_cost_center, - }, + asset.get_gl_dict( + { + "account": fixed_asset_account, + "debit_in_account_currency": asset.gross_purchase_amount, + "debit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), + asset.get_gl_dict( + { + "account": accumulated_depr_account, + "credit_in_account_currency": accumulated_depr_amount, + "credit": accumulated_depr_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), ] profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) if profit_amount: - get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + ) + + if voucher_type and voucher_no: + for entry in gl_entries: + entry["voucher_type"] = voucher_type + entry["voucher_no"] = voucher_no return gl_entries -def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): +def get_gl_entries_on_asset_disposal( + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None +): ( fixed_asset_account, asset, @@ -374,23 +391,38 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) ) = get_asset_details(asset, finance_book) gl_entries = [ - { - "account": fixed_asset_account, - "credit_in_account_currency": asset.gross_purchase_amount, - "credit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center, - }, - { - "account": accumulated_depr_account, - "debit_in_account_currency": accumulated_depr_amount, - "debit": accumulated_depr_amount, - "cost_center": depreciation_cost_center, - }, + asset.get_gl_dict( + { + "account": fixed_asset_account, + "credit_in_account_currency": asset.gross_purchase_amount, + "credit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), + asset.get_gl_dict( + { + "account": accumulated_depr_account, + "debit_in_account_currency": accumulated_depr_amount, + "debit": accumulated_depr_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), ] profit_amount = flt(selling_amount) - flt(value_after_depreciation) if profit_amount: - get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + ) + + if voucher_type and voucher_no: + for entry in gl_entries: + entry["voucher_type"] = voucher_type + entry["voucher_no"] = voucher_no return gl_entries @@ -427,15 +459,21 @@ def get_asset_details(asset, finance_book=None): ) -def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center): +def get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center +): debit_or_credit = "debit" if profit_amount < 0 else "credit" gl_entries.append( - { - "account": disposal_account, - "cost_center": depreciation_cost_center, - debit_or_credit: abs(profit_amount), - debit_or_credit + "_in_account_currency": abs(profit_amount), - } + asset.get_gl_dict( + { + "account": disposal_account, + "cost_center": depreciation_cost_center, + debit_or_credit: abs(profit_amount), + debit_or_credit + "_in_account_currency": abs(profit_amount), + "posting_date": getdate(), + }, + item=asset, + ) ) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 30ff0601e4e..08355f047e5 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -431,7 +431,11 @@ class AssetCapitalization(StockController): asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( - asset, item.asset_value, item.get("finance_book") or self.get("finance_book") + asset, + item.asset_value, + item.get("finance_book") or self.get("finance_book"), + self.get("doctype"), + self.get("name"), ) asset.db_set("disposal_date", self.posting_date) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 2786349f7b6..6e06f52ac65 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -129,6 +129,18 @@ class TestAssetRepair(unittest.TestCase): def test_gl_entries_with_perpetual_inventory(self): set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") + asset_category = frappe.get_doc("Asset Category", "Computers") + asset_category.append( + "accounts", + { + "company_name": "_Test Company with perpetual inventory", + "fixed_asset_account": "_Test Fixed Asset - TCP1", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", + "depreciation_expense_account": "_Test Depreciations - TCP1", + }, + ) + asset_category.save() + asset_repair = create_asset_repair( capitalize_repair_cost=1, stock_consumption=1, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index fbb42fe2f64..fc99d776d4a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -295,131 +295,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e } make_stock_entry() { - var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }); - var me = this; - - if(items.length >= 1){ - me.raw_material_data = []; - me.show_dialog = 1; - let title = __('Transfer Material to Supplier'); - let fields = [ - {fieldtype:'Section Break', label: __('Raw Materials')}, - {fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), - fields: [ - { - fieldtype:'Data', - fieldname:'item_code', - label: __('Item'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Data', - fieldname:'rm_item_code', - label: __('Raw Material'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'qty', - label: __('Quantity'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Data', - read_only:1, - fieldname:'warehouse', - label: __('Reserve Warehouse'), - in_list_view:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'rate', - label: __('Rate'), - hidden:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'amount', - label: __('Amount'), - hidden:1 - }, - { - fieldtype:'Link', - read_only:1, - fieldname:'uom', - label: __('UOM'), - hidden:1 - } - ], - data: me.raw_material_data, - get_data: function() { - return me.raw_material_data; - } - } - ] - - me.dialog = new frappe.ui.Dialog({ - title: title, fields: fields - }); - - if (me.frm.doc['supplied_items']) { - me.frm.doc['supplied_items'].forEach((item, index) => { - if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { - me.raw_material_data.push ({ - 'name':item.name, - 'item_code': item.main_item_code, - 'rm_item_code': item.rm_item_code, - 'item_name': item.rm_item_code, - 'qty': item.required_qty - item.supplied_qty, - 'warehouse':item.reserve_warehouse, - 'rate':item.rate, - 'amount':item.amount, - 'stock_uom':item.stock_uom - }); - me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); - } - }) - } - - me.dialog.get_field('sub_con_rm_items').check_all_rows() - - me.dialog.show() - this.dialog.set_primary_action(__('Transfer'), function() { - me.values = me.dialog.get_values(); - if(me.values) { - me.values.sub_con_rm_items.map((row,i) => { - if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { - let row_id = i+1; - frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id])); - } - }) - me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()) - me.dialog.hide() - } - }); - } - - me.dialog.get_close_btn().on('click', () => { - me.dialog.hide(); - }); - - } - - _make_rm_stock_entry(rm_items) { frappe.call({ method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry", args: { subcontract_order: cur_frm.doc.name, - rm_items: rm_items, order_doctype: cur_frm.doc.doctype - } - , + }, callback: function(r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index acca380672d..fb8f25a0dfc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -441,7 +441,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1180,7 +1179,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-09-07 11:06:46.035093", + "modified": "2022-09-16 17:45:04.954055", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py index dbdc62e9ec7..d089473a16a 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py @@ -53,4 +53,5 @@ def get_chart_data(data, conditions, filters): }, "type": "line", "lineOptions": {"regionFill": 1}, + "fieldtype": "Currency", } diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e9d3c7afba1..22291a35441 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -571,6 +571,11 @@ class AccountsController(TransactionBase): # if user changed the discount percentage then set user's discount percentage ? if pricing_rule_args.get("price_or_product_discount") == "Price": item.set("pricing_rules", pricing_rule_args.get("pricing_rules")) + if pricing_rule_args.get("apply_rule_on_other_items"): + other_items = json.loads(pricing_rule_args.get("apply_rule_on_other_items")) + if other_items and item.item_code not in other_items: + return + item.set("discount_percentage", pricing_rule_args.get("discount_percentage")) item.set("discount_amount", pricing_rule_args.get("discount_amount")) if pricing_rule_args.get("pricing_rule_for") == "Rate": diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 584266d53b1..48fe7cb083d 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -6,6 +6,7 @@ import frappe from frappe import ValidationError, _, msgprint from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, cstr, flt, getdate +from frappe.utils.data import nowtime from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.party import get_party_details @@ -193,16 +194,16 @@ class BuyingController(SubcontractingController): if self.meta.get_field("base_in_words"): if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled(): - amount = self.base_rounded_total + amount = abs(self.base_rounded_total) else: - amount = self.base_grand_total + amount = abs(self.base_grand_total) self.base_in_words = money_in_words(amount, self.company_currency) if self.meta.get_field("in_words"): if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled(): - amount = self.rounded_total + amount = abs(self.rounded_total) else: - amount = self.grand_total + amount = abs(self.grand_total) self.in_words = money_in_words(amount, self.currency) @@ -289,12 +290,16 @@ class BuyingController(SubcontractingController): # Get outgoing rate based on original item cost based on valuation method if not d.get(frappe.scrub(ref_doctype)): + posting_time = self.get("posting_time") + if not posting_time and self.doctype == "Purchase Order": + posting_time = nowtime() + outgoing_rate = get_incoming_rate( { "item_code": d.item_code, "warehouse": d.get("from_warehouse"), "posting_date": self.get("posting_date") or self.get("transation_date"), - "posting_time": self.get("posting_time"), + "posting_time": posting_time, "qty": -1 * flt(d.get("stock_qty")), "serial_no": d.get("serial_no"), "batch_no": d.get("batch_no"), diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index bbd950ed37a..202a880750e 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -770,7 +770,7 @@ def get_item_details(items): item = frappe.qb.DocType("Item") item_list = ( frappe.qb.from_(item) - .select(item.item_code, item.description, item.allow_alternative_item) + .select(item.item_code, item.item_name, item.description, item.allow_alternative_item) .where(item.name.isin(items)) .run(as_dict=True) ) @@ -783,68 +783,93 @@ def get_item_details(items): @frappe.whitelist() -def make_rm_stock_entry(subcontract_order, rm_items, order_doctype="Subcontracting Order"): - rm_items_list = rm_items - - if isinstance(rm_items, str): - rm_items_list = json.loads(rm_items) - elif not rm_items: - frappe.throw(_("No Items available for transfer")) - - if rm_items_list: - fg_items = list(set(item["item_code"] for item in rm_items_list)) - else: - frappe.throw(_("No Items selected for transfer")) - +def make_rm_stock_entry( + subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None +): if subcontract_order: subcontract_order = frappe.get_doc(order_doctype, subcontract_order) - if fg_items: - items = tuple(set(item["rm_item_code"] for item in rm_items_list)) - item_wh = get_item_details(items) + if not rm_items: + if not subcontract_order.supplied_items: + frappe.throw(_("No item available for transfer.")) - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.purpose = "Send to Subcontractor" - if order_doctype == "Purchase Order": - stock_entry.purchase_order = subcontract_order.name - else: - stock_entry.subcontracting_order = subcontract_order.name - stock_entry.supplier = subcontract_order.supplier - stock_entry.supplier_name = subcontract_order.supplier_name - stock_entry.supplier_address = subcontract_order.supplier_address - stock_entry.address_display = subcontract_order.address_display - stock_entry.company = subcontract_order.company - stock_entry.to_warehouse = subcontract_order.supplier_warehouse - stock_entry.set_stock_entry_type() + rm_items = subcontract_order.supplied_items - if order_doctype == "Purchase Order": - rm_detail_field = "po_detail" - else: - rm_detail_field = "sco_rm_detail" + fg_item_code_list = list( + set(item.get("main_item_code") or item.get("item_code") for item in rm_items) + ) - for item_code in fg_items: - for rm_item_data in rm_items_list: - if rm_item_data["item_code"] == item_code: - rm_item_code = rm_item_data["rm_item_code"] - items_dict = { - rm_item_code: { - rm_detail_field: rm_item_data.get("name"), - "item_name": rm_item_data["item_name"], - "description": item_wh.get(rm_item_code, {}).get("description", ""), - "qty": rm_item_data["qty"], - "from_warehouse": rm_item_data["warehouse"], - "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"], - "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + if fg_item_code_list: + rm_item_code_list = tuple(set(item.get("rm_item_code") for item in rm_items)) + item_wh = get_item_details(rm_item_code_list) + + field_no_map, rm_detail_field = "purchase_order", "sco_rm_detail" + if order_doctype == "Purchase Order": + field_no_map, rm_detail_field = "subcontracting_order", "po_detail" + + if target_doc and target_doc.get("items"): + target_doc.items = [] + + stock_entry = get_mapped_doc( + order_doctype, + subcontract_order.name, + { + order_doctype: { + "doctype": "Stock Entry", + "field_map": { + "to_warehouse": "supplier_warehouse", + }, + "field_no_map": [field_no_map], + "validation": { + "docstatus": ["=", 1], + }, + }, + }, + target_doc, + ignore_child_tables=True, + ) + + stock_entry.purpose = "Send to Subcontractor" + + if order_doctype == "Purchase Order": + stock_entry.purchase_order = subcontract_order.name + else: + stock_entry.subcontracting_order = subcontract_order.name + + stock_entry.set_stock_entry_type() + + for fg_item_code in fg_item_code_list: + for rm_item in rm_items: + + if rm_item.get("main_item_code") or rm_item.get("item_code") == fg_item_code: + rm_item_code = rm_item.get("rm_item_code") + + items_dict = { + rm_item_code: { + rm_detail_field: rm_item.get("name"), + "item_name": rm_item.get("item_name") + or item_wh.get(rm_item_code, {}).get("item_name", ""), + "description": item_wh.get(rm_item_code, {}).get("description", ""), + "qty": rm_item.get("qty") + or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0), + "from_warehouse": rm_item.get("warehouse") or rm_item.get("reserve_warehouse"), + "to_warehouse": subcontract_order.supplier_warehouse, + "stock_uom": rm_item.get("stock_uom"), + "serial_no": rm_item.get("serial_no"), + "batch_no": rm_item.get("batch_no"), + "main_item_code": fg_item_code, + "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + } } - } - stock_entry.add_to_stock_entry_detail(items_dict) - return stock_entry.as_dict() - else: - frappe.throw(_("No Items selected for transfer")) - return subcontract_order.name + + stock_entry.add_to_stock_entry_detail(items_dict) + + if target_doc: + return stock_entry + else: + return stock_entry.as_dict() + else: + frappe.throw(_("No Items selected for transfer.")) def add_items_in_ste( diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index 116db2f5a27..7cd1710a7f2 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -44,7 +44,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { }, { fieldname: "opportunity_source", - label: __("Oppoturnity Source"), + label: __("Opportunity Source"), fieldtype: "Link", options: "Lead Source", }, @@ -62,4 +62,4 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { default: frappe.defaults.get_user_default("Company") } ] -}; \ No newline at end of file +}; diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 70637d3ef20..ff84991c36e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1019,7 +1019,6 @@ def get_bom_items_as_dict( where bom_item.docstatus < 2 and bom.name = %(bom)s - and ifnull(item.has_variants, 0) = 0 and item.is_stock_item in (1, {is_stock_item}) {where_conditions} group by item_code, stock_uom diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 66d458bf750..4bb4dcc648e 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -8,6 +8,7 @@ import json import frappe from frappe import _, msgprint from frappe.model.document import Document +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, ceil, @@ -20,6 +21,7 @@ from frappe.utils import ( nowdate, ) from frappe.utils.csvutils import build_csv_response +from pypika.terms import ExistsCriterion from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children from erpnext.manufacturing.doctype.bom.bom import validate_bom_no @@ -100,39 +102,46 @@ class ProductionPlan(Document): @frappe.whitelist() def get_pending_material_requests(self): """Pull Material Requests that are pending based on criteria selected""" - mr_filter = item_filter = "" + + bom = frappe.qb.DocType("BOM") + mr = frappe.qb.DocType("Material Request") + mr_item = frappe.qb.DocType("Material Request Item") + + pending_mr_query = ( + frappe.qb.from_(mr) + .from_(mr_item) + .select(mr.name, mr.transaction_date) + .distinct() + .where( + (mr_item.parent == mr.name) + & (mr.material_request_type == "Manufacture") + & (mr.docstatus == 1) + & (mr.status != "Stopped") + & (mr.company == self.company) + & (mr_item.qty > IfNull(mr_item.ordered_qty, 0)) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == mr_item.item_code) & (bom.is_active == 1)) + ) + ) + ) + ) + if self.from_date: - mr_filter += " and mr.transaction_date >= %(from_date)s" + pending_mr_query = pending_mr_query.where(mr.transaction_date >= self.from_date) + if self.to_date: - mr_filter += " and mr.transaction_date <= %(to_date)s" + pending_mr_query = pending_mr_query.where(mr.transaction_date <= self.to_date) + if self.warehouse: - mr_filter += " and mr_item.warehouse = %(warehouse)s" + pending_mr_query = pending_mr_query.where(mr_item.warehouse == self.warehouse) if self.item_code: - item_filter += " and mr_item.item_code = %(item)s" + pending_mr_query = pending_mr_query.where(mr_item.item_code == self.item_code) - pending_mr = frappe.db.sql( - """ - select distinct mr.name, mr.transaction_date - from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where mr_item.parent = mr.name - and mr.material_request_type = "Manufacture" - and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s - and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1} - and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1)) - """.format( - mr_filter, item_filter - ), - { - "from_date": self.from_date, - "to_date": self.to_date, - "warehouse": self.warehouse, - "item": self.item_code, - "company": self.company, - }, - as_dict=1, - ) + pending_mr = pending_mr_query.run(as_dict=True) self.add_mr_in_table(pending_mr) @@ -160,16 +169,17 @@ class ProductionPlan(Document): so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)] return so_mr_list - def get_bom_item(self): + def get_bom_item_condition(self): """Check if Item or if its Template has a BOM.""" - bom_item = None + bom_item_condition = None has_bom = frappe.db.exists({"doctype": "BOM", "item": self.item_code, "docstatus": 1}) + if not has_bom: + bom = frappe.qb.DocType("BOM") template_item = frappe.db.get_value("Item", self.item_code, ["variant_of"]) - bom_item = ( - "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item - ) - return bom_item + bom_item_condition = bom.item == template_item or None + + return bom_item_condition def get_so_items(self): # Check for empty table or empty rows @@ -178,46 +188,75 @@ class ProductionPlan(Document): so_list = self.get_so_mr_list("sales_order", "sales_orders") - item_condition = "" - bom_item = "bom.item = so_item.item_code" - if self.item_code and frappe.db.exists("Item", self.item_code): - bom_item = self.get_bom_item() or bom_item - item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) + bom = frappe.qb.DocType("BOM") + so_item = frappe.qb.DocType("Sales Order Item") - items = frappe.db.sql( - """ - select - distinct parent, item_code, warehouse, - (qty - work_order_qty) * conversion_factor as pending_qty, - description, name - from - `tabSales Order Item` so_item - where - parent in (%s) and docstatus = 1 and qty > work_order_qty - and exists (select name from `tabBOM` bom where %s - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(so_list)), bom_item, item_condition), - tuple(so_list), - as_dict=1, + items_subquery = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1) + items_query = ( + frappe.qb.from_(so_item) + .select( + so_item.parent, + so_item.item_code, + so_item.warehouse, + ( + (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor + ).as_("pending_qty"), + so_item.description, + so_item.name, + ) + .distinct() + .where( + (so_item.parent.isin(so_list)) + & (so_item.docstatus == 1) + & (so_item.qty > so_item.work_order_qty) + ) + ) + + if self.item_code and frappe.db.exists("Item", self.item_code): + items_query = items_query.where(so_item.item_code == self.item_code) + items_subquery = items_subquery.where( + self.get_bom_item_condition() or bom.item == so_item.item_code + ) + + items_query = items_query.where(ExistsCriterion(items_subquery)) + + items = items_query.run(as_dict=True) + + pi = frappe.qb.DocType("Packed Item") + + packed_items_query = ( + frappe.qb.from_(so_item) + .from_(pi) + .select( + pi.parent, + pi.item_code, + pi.warehouse.as_("warehouse"), + (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty).as_("pending_qty"), + pi.parent_item, + pi.description, + so_item.name, + ) + .distinct() + .where( + (so_item.parent == pi.parent) + & (so_item.docstatus == 1) + & (pi.parent_item == so_item.item_code) + & (so_item.parent.isin(so_list)) + & (so_item.qty > so_item.work_order_qty) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == pi.item_code) & (bom.is_active == 1)) + ) + ) + ) ) if self.item_code: - item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) + packed_items_query = packed_items_query.where(so_item.item_code == self.item_code) - packed_items = frappe.db.sql( - """select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, - (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty) - as pending_qty, pi.parent_item, pi.description, so_item.name - from `tabSales Order Item` so_item, `tabPacked Item` pi - where so_item.parent = pi.parent and so_item.docstatus = 1 - and pi.parent_item = so_item.item_code - and so_item.parent in (%s) and so_item.qty > so_item.work_order_qty - and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(so_list)), item_condition), - tuple(so_list), - as_dict=1, - ) + packed_items = packed_items_query.run(as_dict=True) self.add_items(items + packed_items) self.calculate_total_planned_qty() @@ -233,22 +272,39 @@ class ProductionPlan(Document): mr_list = self.get_so_mr_list("material_request", "material_requests") - item_condition = "" - if self.item_code: - item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code)) + bom = frappe.qb.DocType("BOM") + mr_item = frappe.qb.DocType("Material Request Item") - items = frappe.db.sql( - """select distinct parent, name, item_code, warehouse, description, - (qty - ordered_qty) * conversion_factor as pending_qty - from `tabMaterial Request Item` mr_item - where parent in (%s) and docstatus = 1 and qty > ordered_qty - and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(mr_list)), item_condition), - tuple(mr_list), - as_dict=1, + items_query = ( + frappe.qb.from_(mr_item) + .select( + mr_item.parent, + mr_item.name, + mr_item.item_code, + mr_item.warehouse, + mr_item.description, + ((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor).as_("pending_qty"), + ) + .distinct() + .where( + (mr_item.parent.isin(mr_list)) + & (mr_item.docstatus == 1) + & (mr_item.qty > mr_item.ordered_qty) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == mr_item.item_code) & (bom.is_active == 1)) + ) + ) + ) ) + if self.item_code: + items_query = items_query.where(mr_item.item_code == self.item_code) + + items = items_query.run(as_dict=True) + self.add_items(items) self.calculate_total_planned_qty() @@ -819,29 +875,46 @@ def download_raw_materials(doc, warehouses=None): def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1): - for d in frappe.db.sql( - """select bei.item_code, item.default_bom as bom, - ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, - bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, - item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, - item.purchase_uom, item_uom.conversion_factor, item.safety_stock - from - `tabBOM Explosion Item` bei - JOIN `tabBOM` bom ON bom.name = bei.parent - JOIN `tabItem` item ON item.name = bei.item_code - LEFT JOIN `tabItem Default` item_default - ON item_default.parent = item.name and item_default.company=%s - LEFT JOIN `tabUOM Conversion Detail` item_uom - ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom - where - bei.docstatus < 2 - and bom.name=%s and item.is_stock_item in (1, {0}) - group by bei.item_code, bei.stock_uom""".format( - 0 if include_non_stock_items else 1 - ), - (planned_qty, company, bom_no), - as_dict=1, - ): + bei = frappe.qb.DocType("BOM Explosion Item") + bom = frappe.qb.DocType("BOM") + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + item_uom = frappe.qb.DocType("UOM Conversion Detail") + + data = ( + frappe.qb.from_(bei) + .join(bom) + .on(bom.name == bei.parent) + .join(item) + .on(item.name == bei.item_code) + .left_join(item_default) + .on((item_default.parent == item.name) & (item_default.company == company)) + .left_join(item_uom) + .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom)) + .select( + (IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"), + item.item_name, + item.name.as_("item_code"), + bei.description, + bei.stock_uom, + item.min_order_qty, + bei.source_warehouse, + item.default_material_request_type, + item.min_order_qty, + item_default.default_warehouse, + item.purchase_uom, + item_uom.conversion_factor, + item.safety_stock, + ) + .where( + (bei.docstatus < 2) + & (bom.name == bom_no) + & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) + ) + .groupby(bei.item_code, bei.stock_uom) + ).run(as_dict=True) + + for d in data: if not d.conversion_factor and d.purchase_uom: d.conversion_factor = get_uom_conversion_factor(d.item_code, d.purchase_uom) item_details.setdefault(d.get("item_code"), d) @@ -866,33 +939,47 @@ def get_subitems( parent_qty, planned_qty=1, ): - items = frappe.db.sql( - """ - SELECT - bom_item.item_code, default_material_request_type, item.item_name, - ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, - item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, - item.default_bom as default_bom, bom_item.description as description, - bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock, - item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor - FROM - `tabBOM Item` bom_item - JOIN `tabBOM` bom ON bom.name = bom_item.parent - JOIN `tabItem` item ON bom_item.item_code = item.name - LEFT JOIN `tabItem Default` item_default - ON item.name = item_default.parent and item_default.company = %(company)s - LEFT JOIN `tabUOM Conversion Detail` item_uom - ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom - where - bom.name = %(bom)s - and bom_item.docstatus < 2 - and item.is_stock_item in (1, {0}) - group by bom_item.item_code""".format( - 0 if include_non_stock_items else 1 - ), - {"bom": bom_no, "parent_qty": parent_qty, "planned_qty": planned_qty, "company": company}, - as_dict=1, - ) + bom_item = frappe.qb.DocType("BOM Item") + bom = frappe.qb.DocType("BOM") + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + item_uom = frappe.qb.DocType("UOM Conversion Detail") + + items = ( + frappe.qb.from_(bom_item) + .join(bom) + .on(bom.name == bom_item.parent) + .join(item) + .on(bom_item.item_code == item.name) + .left_join(item_default) + .on((item.name == item_default.parent) & (item_default.company == company)) + .left_join(item_uom) + .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom)) + .select( + bom_item.item_code, + item.default_material_request_type, + item.item_name, + IfNull(parent_qty * Sum(bom_item.stock_qty / IfNull(bom.quantity, 1)) * planned_qty, 0).as_( + "qty" + ), + item.is_sub_contracted_item.as_("is_sub_contracted"), + bom_item.source_warehouse, + item.default_bom.as_("default_bom"), + bom_item.description.as_("description"), + bom_item.stock_uom.as_("stock_uom"), + item.min_order_qty.as_("min_order_qty"), + item.safety_stock.as_("safety_stock"), + item_default.default_warehouse, + item.purchase_uom, + item_uom.conversion_factor, + ) + .where( + (bom.name == bom_no) + & (bom_item.docstatus < 2) + & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) + ) + .groupby(bom_item.item_code) + ).run(as_dict=True) for d in items: if not data.get("include_exploded_items") or not d.default_bom: @@ -980,48 +1067,69 @@ def get_material_request_items( def get_sales_orders(self): - so_filter = item_filter = "" - bom_item = "bom.item = so_item.item_code" + bom = frappe.qb.DocType("BOM") + pi = frappe.qb.DocType("Packed Item") + so = frappe.qb.DocType("Sales Order") + so_item = frappe.qb.DocType("Sales Order Item") + + open_so_subquery1 = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1) + + open_so_subquery2 = ( + frappe.qb.from_(pi) + .select(pi.name) + .where( + (pi.parent == so.name) + & (pi.parent_item == so_item.item_code) + & ( + ExistsCriterion( + frappe.qb.from_(bom).select(bom.name).where((bom.item == pi.item_code) & (bom.is_active == 1)) + ) + ) + ) + ) + + open_so_query = ( + frappe.qb.from_(so) + .from_(so_item) + .select(so.name, so.transaction_date, so.customer, so.base_grand_total) + .distinct() + .where( + (so_item.parent == so.name) + & (so.docstatus == 1) + & (so.status.notin(["Stopped", "Closed"])) + & (so.company == self.company) + & (so_item.qty > so_item.work_order_qty) + ) + ) date_field_mapper = { - "from_date": (">=", "so.transaction_date"), - "to_date": ("<=", "so.transaction_date"), - "from_delivery_date": (">=", "so_item.delivery_date"), - "to_delivery_date": ("<=", "so_item.delivery_date"), + "from_date": self.from_date >= so.transaction_date, + "to_date": self.to_date <= so.transaction_date, + "from_delivery_date": self.from_delivery_date >= so_item.delivery_date, + "to_delivery_date": self.to_delivery_date <= so_item.delivery_date, } for field, value in date_field_mapper.items(): if self.get(field): - so_filter += f" and {value[1]} {value[0]} %({field})s" + open_so_query = open_so_query.where(value) - for field in ["customer", "project", "sales_order_status"]: + for field in ("customer", "project", "sales_order_status"): if self.get(field): so_field = "status" if field == "sales_order_status" else field - so_filter += f" and so.{so_field} = %({field})s" + open_so_query = open_so_query.where(so[so_field] == self.get(field)) if self.item_code and frappe.db.exists("Item", self.item_code): - bom_item = self.get_bom_item() or bom_item - item_filter += " and so_item.item_code = %(item_code)s" + open_so_query = open_so_query.where(so_item.item_code == self.item_code) + open_so_subquery1 = open_so_subquery1.where( + self.get_bom_item_condition() or bom.item == so_item.item_code + ) - open_so = frappe.db.sql( - f""" - select distinct so.name, so.transaction_date, so.customer, so.base_grand_total - from `tabSales Order` so, `tabSales Order Item` so_item - where so_item.parent = so.name - and so.docstatus = 1 and so.status not in ('Stopped', 'Closed') - and so.company = %(company)s - and so_item.qty > so_item.work_order_qty {so_filter} {item_filter} - and (exists (select name from `tabBOM` bom where {bom_item} - and bom.is_active = 1) - or exists (select name from `tabPacked Item` pi - where pi.parent = so.name and pi.parent_item = so_item.item_code - and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1))) - """, - self.as_dict(), - as_dict=1, + open_so_query = open_so_query.where( + (ExistsCriterion(open_so_subquery1) | ExistsCriterion(open_so_subquery2)) ) + open_so = open_so_query.run(as_dict=True) + return open_so @@ -1030,37 +1138,34 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): if isinstance(row, str): row = frappe._dict(json.loads(row)) - company = frappe.db.escape(company) - conditions, warehouse = "", "" + bin = frappe.qb.DocType("Bin") + wh = frappe.qb.DocType("Warehouse") + + subquery = frappe.qb.from_(wh).select(wh.name).where(wh.company == company) - conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format( - company - ) if not all_warehouse: warehouse = for_warehouse or row.get("source_warehouse") or row.get("default_warehouse") if warehouse: lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - conditions = """ and warehouse in (select name from `tabWarehouse` - where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse and company = {2}) - """.format( - lft, rgt, company - ) + subquery = subquery.where((wh.lft >= lft) & (wh.rgt <= rgt) & (wh.name == bin.warehouse)) - return frappe.db.sql( - """ select ifnull(sum(projected_qty),0) as projected_qty, - ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, - ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, - ifnull(sum(planned_qty),0) as planned_qty - from `tabBin` where item_code = %(item_code)s {conditions} - group by item_code, warehouse - """.format( - conditions=conditions - ), - {"item_code": row["item_code"]}, - as_dict=1, + query = ( + frappe.qb.from_(bin) + .select( + bin.warehouse, + IfNull(Sum(bin.projected_qty), 0).as_("projected_qty"), + IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), + IfNull(Sum(bin.ordered_qty), 0).as_("ordered_qty"), + IfNull(Sum(bin.reserved_qty_for_production), 0).as_("reserved_qty_for_production"), + IfNull(Sum(bin.planned_qty), 0).as_("planned_qty"), + ) + .where((bin.item_code == row["item_code"]) & (bin.warehouse.isin(subquery))) + .groupby(bin.item_code, bin.warehouse) ) + return query.run(as_dict=True) + @frappe.whitelist() def get_so_details(sales_order): diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 1d2d1bd9a84..60e63980724 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( ) from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_se_from_wo +from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -610,15 +611,21 @@ class TestProductionPlan(FrappeTestCase): """ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + make_stock_entry(item_code="_Test Item", target="Work In Progress - _TC", qty=2, basic_rate=100) make_stock_entry( - item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 - ) - make_stock_entry( - item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100 + item_code="_Test Item Home Desktop 100", target="Work In Progress - _TC", qty=4, basic_rate=100 ) - item = "Test Production Item 1" - so = make_sales_order(item_code=item, qty=1) + item = "_Test FG Item" + + make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=1) + + so = make_sales_order(item_code=item, qty=2) + + dn = make_delivery_note(so.name) + dn.items[0].qty = 1 + dn.save() + dn.submit() pln = create_production_plan( company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index ec4b25c859f..550445c1f77 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -146,7 +146,7 @@ def get_bom_data(filters): ) ) else: - query = query.where(bin.warehouse == frappe.db.escape(filters.get("warehouse"))) + query = query.where(bin.warehouse == filters.get("warehouse")) return query.run(as_dict=True) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 34e9826305e..1e1b4356008 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -4,6 +4,8 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Floor, Sum +from pypika.terms import ExistsCriterion def execute(filters=None): @@ -11,7 +13,6 @@ def execute(filters=None): filters = {} columns = get_columns() - data = get_bom_stock(filters) return columns, data @@ -33,59 +34,57 @@ def get_columns(): def get_bom_stock(filters): - conditions = "" - bom = filters.get("bom") - - table = "`tabBOM Item`" - qty_field = "stock_qty" - - qty_to_produce = filters.get("qty_to_produce", 1) - if int(qty_to_produce) <= 0: + qty_to_produce = filters.get("qty_to_produce") or 1 + if int(qty_to_produce) < 0: frappe.throw(_("Quantity to Produce can not be less than Zero")) if filters.get("show_exploded_view"): - table = "`tabBOM Explosion Item`" + bom_item_table = "BOM Explosion Item" + else: + bom_item_table = "BOM Item" + + bin = frappe.qb.DocType("Bin") + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType(bom_item_table) + + query = ( + frappe.qb.from_(bom) + .inner_join(bom_item) + .on(bom.name == bom_item.parent) + .left_join(bin) + .on(bom_item.item_code == bin.item_code) + .select( + bom_item.item_code, + bom_item.description, + bom_item.stock_qty, + bom_item.stock_uom, + bom_item.stock_qty * qty_to_produce / bom.quantity, + Sum(bin.actual_qty).as_("actual_qty"), + Sum(Floor(bin.actual_qty / (bom_item.stock_qty * qty_to_produce / bom.quantity))), + ) + .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) + .groupby(bom_item.item_code) + ) if filters.get("warehouse"): warehouse_details = frappe.db.get_value( "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) + if warehouse_details: - conditions += ( - " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where( + (wh.lft >= warehouse_details.lft) + & (wh.rgt <= warehouse_details.rgt) + & (bin.warehouse == wh.name) + ) + ) ) else: - conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse")) + query = query.where(bin.warehouse == filters.get("warehouse")) - else: - conditions += "" - - return frappe.db.sql( - """ - SELECT - bom_item.item_code, - bom_item.description , - bom_item.{qty_field}, - bom_item.stock_uom, - bom_item.{qty_field} * {qty_to_produce} / bom.quantity, - sum(ledger.actual_qty) as actual_qty, - sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) - FROM - `tabBOM` AS bom INNER JOIN {table} AS bom_item - ON bom.name = bom_item.parent - LEFT JOIN `tabBin` AS ledger - ON bom_item.item_code = ledger.item_code - {conditions} - WHERE - bom_item.parent = {bom} and bom_item.parenttype='BOM' - - GROUP BY bom_item.item_code""".format( - qty_field=qty_field, - table=table, - conditions=conditions, - bom=frappe.db.escape(bom), - qty_to_produce=qty_to_produce or 1, - ) - ) + return query.run() diff --git a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py index da283435b9f..70a1850fd0f 100644 --- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py +++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py @@ -64,22 +64,21 @@ def get_columns(filters): def get_data(filters): - cond = "1=1" + wo = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(wo) + .select(wo.name.as_("work_order"), wo.qty, wo.produced_qty, wo.production_item, wo.bom_no) + .where((wo.produced_qty > wo.qty) & (wo.docstatus == 1)) + ) if filters.get("bom_no") and not filters.get("work_order"): - cond += " and bom_no = '%s'" % filters.get("bom_no") + query = query.where(wo.bom_no == filters.get("bom_no")) if filters.get("work_order"): - cond += " and name = '%s'" % filters.get("work_order") + query = query.where(wo.name == filters.get("work_order")) results = [] - for d in frappe.db.sql( - """ select name as work_order, qty, produced_qty, production_item, bom_no - from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format( - cond - ), - as_dict=1, - ): + for d in query.run(as_dict=True): results.append(d) for data in frappe.get_all( @@ -95,16 +94,17 @@ def get_data(filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_work_orders(doctype, txt, searchfield, start, page_len, filters): - cond = "1=1" - if filters.get("bom_no"): - cond += " and bom_no = '%s'" % filters.get("bom_no") - - return frappe.db.sql( - """select name from `tabWork Order` - where name like %(name)s and {0} and produced_qty > qty and docstatus = 1 - order by name limit {2} offset {1}""".format( - cond, start, page_len - ), - {"name": "%%%s%%" % txt}, - as_list=1, + wo = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(wo) + .select(wo.name) + .where((wo.name.like(f"{txt}%")) & (wo.produced_qty > wo.qty) & (wo.docstatus == 1)) + .orderby(wo.name) + .limit(page_len) + .offset(start) ) + + if filters.get("bom_no"): + query = query.where(wo.bom_no == filters.get("bom_no")) + + return query.run(as_list=True) diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index 7500744c228..d3bce831551 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -96,38 +96,39 @@ class ForecastingReport(ExponentialSmoothingForecast): value["avg"] = flt(sum(list_of_period_value)) / flt(sum(total_qty)) def get_data_for_forecast(self): - cond = "" - if self.filters.item_code: - cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code)) - - warehouses = [] - if self.filters.warehouse: - warehouses = get_child_warehouses(self.filters.warehouse) - cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses))) - - input_data = [self.filters.from_date, self.filters.company] - if warehouses: - input_data.extend(warehouses) + parent = frappe.qb.DocType(self.doctype) + child = frappe.qb.DocType(self.child_doctype) date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date" - return frappe.db.sql( - """ - SELECT - so.{date_field} as posting_date, soi.item_code, soi.warehouse, - soi.item_name, soi.stock_qty as qty, soi.base_amount as amount - FROM - `tab{doc}` so, `tab{child_doc}` soi - WHERE - so.docstatus = 1 AND so.name = soi.parent AND - so.{date_field} < %s AND so.company = %s {cond} - """.format( - doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond - ), - tuple(input_data), - as_dict=1, + query = ( + frappe.qb.from_(parent) + .from_(child) + .select( + parent[date_field].as_("posting_date"), + child.item_code, + child.warehouse, + child.item_name, + child.stock_qty.as_("qty"), + child.base_amount.as_("amount"), + ) + .where( + (parent.docstatus == 1) + & (parent.name == child.parent) + & (parent[date_field] < self.filters.from_date) + & (parent.company == self.filters.company) + ) ) + if self.filters.item_code: + query = query.where(child.item_code == self.filters.item_code) + + if self.filters.warehouse: + warehouses = get_child_warehouses(self.filters.warehouse) or [] + query = query.where(child.warehouse.isin(warehouses)) + + return query.run(as_dict=True) + def prepare_final_data(self): self.data = [] diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py index 5083b7369de..63c2d97d574 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -85,8 +85,8 @@ def get_chart_data(job_card_details, filters): open_job_cards.append(periodic_data.get("Open").get(d)) completed.append(periodic_data.get("Completed").get(d)) - datasets.append({"name": "Open", "values": open_job_cards}) - datasets.append({"name": "Completed", "values": completed}) + datasets.append({"name": _("Open"), "values": open_job_cards}) + datasets.append({"name": _("Completed"), "values": completed}) chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index 140488820a5..16c25ce7e6d 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -4,42 +4,10 @@ import frappe from frappe import _ +from pypika import Order from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses -# and bom_no is not null and bom_no !='' - -mapper = { - "Sales Order": { - "fields": """ item_code as production_item, item_name as production_item_name, stock_uom, - stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse, - `tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """, - "filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty - and `tabSales Order`.per_delivered < 100.0""", - }, - "Material Request": { - "fields": """ item_code as production_item, item_name as production_item_name, stock_uom, - stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse, - `tabMaterial Request Item`.schedule_date """, - "filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100 - and `tabMaterial Request`.material_request_type = 'Manufacture' """, - }, - "Work Order": { - "fields": """ production_item, item_name as production_item_name, planned_start_date, - stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """, - "filters": "docstatus = 1 and status not in ('Completed', 'Stopped')", - }, -} - -order_mapper = { - "Sales Order": { - "Delivery Date": "`tabSales Order Item`.delivery_date asc", - "Total Amount": "`tabSales Order`.base_grand_total desc", - }, - "Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"}, - "Work Order": {"Planned Start Date": "planned_start_date asc"}, -} - def execute(filters=None): return ProductionPlanReport(filters).execute_report() @@ -63,40 +31,78 @@ class ProductionPlanReport(object): return self.columns, self.data def get_open_orders(self): - doctype = ( - "`tabWork Order`" - if self.filters.based_on == "Work Order" - else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on) - ) + doctype, order_by = self.filters.based_on, self.filters.order_by - filters = mapper.get(self.filters.based_on)["filters"] - filters = self.prepare_other_conditions(filters, self.filters.based_on) - order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by]) + parent = frappe.qb.DocType(doctype) + query = None - self.orders = frappe.db.sql( - """ SELECT {fields} from {doctype} - WHERE {filters} {order_by}""".format( - doctype=doctype, - filters=filters, - order_by=order_by, - fields=mapper.get(self.filters.based_on)["fields"], - ), - tuple(self.filters.docnames), - as_dict=1, - ) + if doctype == "Work Order": + query = ( + frappe.qb.from_(parent) + .select( + parent.production_item, + parent.item_name.as_("production_item_name"), + parent.planned_start_date, + parent.stock_uom, + parent.qty.as_("qty_to_manufacture"), + parent.name, + parent.bom_no, + parent.fg_warehouse.as_("warehouse"), + ) + .where(parent.status.notin(["Completed", "Stopped"])) + ) - def prepare_other_conditions(self, filters, doctype): - if self.filters.docnames: - field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype) - filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames))) + if order_by == "Planned Start Date": + query = query.orderby(parent.planned_start_date, order=Order.asc) - if doctype != "Work Order": - filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype) + if self.filters.docnames: + query = query.where(parent.name.isin(self.filters.docnames)) + + else: + child = frappe.qb.DocType(f"{doctype} Item") + query = ( + frappe.qb.from_(parent) + .from_(child) + .select( + child.bom_no, + child.stock_uom, + child.warehouse, + child.parent.as_("name"), + child.item_code.as_("production_item"), + child.stock_qty.as_("qty_to_manufacture"), + child.item_name.as_("production_item_name"), + ) + .where(parent.name == child.parent) + ) + + if self.filters.docnames: + query = query.where(child.parent.isin(self.filters.docnames)) + + if doctype == "Sales Order": + query = query.select( + child.delivery_date, + parent.base_grand_total, + ).where((child.stock_qty > child.produced_qty) & (parent.per_delivered < 100.0)) + + if order_by == "Delivery Date": + query = query.orderby(child.delivery_date, order=Order.asc) + elif order_by == "Total Amount": + query = query.orderby(parent.base_grand_total, order=Order.desc) + + elif doctype == "Material Request": + query = query.select(child.schedule_date,).where( + (parent.per_ordered < 100) & (parent.material_request_type == "Manufacture") + ) + + if order_by == "Required Date": + query = query.orderby(child.schedule_date, order=Order.asc) + + query = query.where(parent.docstatus == 1) if self.filters.company: - filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company)) + query = query.where(parent.company == self.filters.company) - return filters + self.orders = query.run(as_dict=True) def get_raw_materials(self): if not self.orders: @@ -134,29 +140,29 @@ class ProductionPlanReport(object): bom_nos.append(bom_no) - bom_doctype = ( + bom_item_doctype = ( "BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item" ) - qty_field = ( - "qty_consumed_per_unit" - if self.filters.include_subassembly_raw_materials - else "(bom_item.qty / bom.quantity)" - ) + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType(bom_item_doctype) - raw_materials = frappe.db.sql( - """ SELECT bom_item.parent, bom_item.item_code, - bom_item.item_name as raw_material_name, {0} as required_qty_per_unit - FROM - `tabBOM` as bom, `tab{1}` as bom_item - WHERE - bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1 - """.format( - qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos)) - ), - tuple(bom_nos), - as_dict=1, - ) + if self.filters.include_subassembly_raw_materials: + qty_field = bom_item.qty_consumed_per_unit + else: + qty_field = bom_item.qty / bom.quantity + + raw_materials = ( + frappe.qb.from_(bom) + .from_(bom_item) + .select( + bom_item.parent, + bom_item.item_code, + bom_item.item_name.as_("raw_material_name"), + qty_field.as_("required_qty_per_unit"), + ) + .where((bom_item.parent.isin(bom_nos)) & (bom_item.parent == bom.name) & (bom.docstatus == 1)) + ).run(as_dict=True) if not raw_materials: return diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index 2368bfdf2c6..41ffcbb1904 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -83,6 +83,7 @@ def get_chart_based_on_status(data): for d in data: status_wise_data[d.status] += 1 + labels = [_(label) for label in labels] values = [status_wise_data[label] for label in labels] chart = { @@ -95,7 +96,7 @@ def get_chart_based_on_status(data): def get_chart_based_on_age(data): - labels = ["0-30 Days", "30-60 Days", "60-90 Days", "90 Above"] + labels = [_("0-30 Days"), _("30-60 Days"), _("60-90 Days"), _("90 Above")] age_wise_data = {"0-30 Days": 0, "30-60 Days": 0, "60-90 Days": 0, "90 Above": 0} @@ -135,8 +136,8 @@ def get_chart_based_on_qty(data, filters): pending.append(periodic_data.get("Pending").get(d)) completed.append(periodic_data.get("Completed").get(d)) - datasets.append({"name": "Pending", "values": pending}) - datasets.append({"name": "Completed", "values": completed}) + datasets.append({"name": _("Pending"), "values": pending}) + datasets.append({"name": _("Completed"), "values": completed}) chart = { "data": {"labels": labels, "datasets": datasets}, diff --git a/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py b/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py index 032aeccc23d..ec72527552c 100644 --- a/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py +++ b/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py @@ -12,7 +12,7 @@ def execute(): frappe.qb.from_(dt) .select(dt.name, dt.notes, dt.modified_by, dt.modified) .where(dt.notes.isnotnull() & dt.notes != "") - ).run() + ).run(as_dict=True) for d in records: if strip_html(cstr(d.notes)).strip(): diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 4f19bbd5163..c48ed918024 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -152,6 +152,7 @@ function open_form(frm, doctype, child_doctype, parentfield) { new_child_doc.parentfield = parentfield; new_child_doc.parenttype = doctype; new_doc[parentfield] = [new_child_doc]; + new_doc.project = frm.doc.name; frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); diff --git a/erpnext/projects/report/project_summary/project_summary.py b/erpnext/projects/report/project_summary/project_summary.py index 606c0c2d81d..7a35fd236a0 100644 --- a/erpnext/projects/report/project_summary/project_summary.py +++ b/erpnext/projects/report/project_summary/project_summary.py @@ -91,9 +91,9 @@ def get_chart_data(data): "data": { "labels": labels[:30], "datasets": [ - {"name": "Overdue", "values": overdue[:30]}, - {"name": "Completed", "values": completed[:30]}, - {"name": "Total Tasks", "values": total[:30]}, + {"name": _("Overdue"), "values": overdue[:30]}, + {"name": _("Completed"), "values": completed[:30]}, + {"name": _("Total Tasks"), "values": total[:30]}, ], }, "type": "bar", diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c0a8c9e088c..c17610b58a2 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1492,7 +1492,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.model.set_value(child.doctype, child.name, "rate", value); } + if (key === "pricing_rules") { + frappe.model.set_value(child.doctype, child.name, key, value); + } + if (key !== "free_item_data") { + if (child.apply_rule_on_other_items && JSON.parse(child.apply_rule_on_other_items).length) { + if (!in_list(JSON.parse(child.apply_rule_on_other_items), child.item_code)) { + continue; + } + } + frappe.model.set_value(child.doctype, child.name, key, value); } } @@ -1510,11 +1520,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.remove_pricing_rule(frappe.get_doc(child.doctype, child.name)); } - if (child.free_item_data.length > 0) { + if (child.free_item_data && child.free_item_data.length > 0) { this.apply_product_discount(child); } - if (child.apply_rule_on_other_items) { + if (child.apply_rule_on_other_items && JSON.parse(child.apply_rule_on_other_items).length) { items_rule_dict[child.name] = child; } } @@ -1530,11 +1540,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe for(var k in args) { let data = args[k]; - if (data && data.apply_rule_on_other_items) { + if (data && data.apply_rule_on_other_items && JSON.parse(data.apply_rule_on_other_items)) { me.frm.doc.items.forEach(d => { - if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { + if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on])) { for(var k in data) { - if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'price' || k === 'pricing_rules')) { + if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'Price' || k === 'pricing_rules')) { frappe.model.set_value(d.doctype, d.name, k, data[k]); } } diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index b643ccae947..1c3f43e9cf4 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -671,7 +671,7 @@ frappe.help.help_links["List/Item"] = [ label: "Item Valuation", url: docsUrl + - "user/manual/en/stock/articles/item-valuation-fifo-and-moving-average", + "user/manual/en/stock/articles/calculation-of-valuation-rate-in-fifo-and-moving-average", }, ]; diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index a6bff2c148d..83b108b8746 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -21,6 +21,11 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.items_table_name = opts.items_table_name || "items"; this.items_table = this.frm.doc[this.items_table_name]; + // optional sound name to play when scan either fails or passes. + // see https://frappeframework.com/docs/v14/user/en/python-api/hooks#sounds + this.success_sound = opts.play_success_sound; + this.fail_sound = opts.play_fail_sound; + // any API that takes `search_value` as input and returns dictionary as follows // { // item_code: "HORSESHOE", // present if any item was found @@ -54,19 +59,24 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { if (!data || Object.keys(data).length === 0) { this.show_alert(__("Cannot find Item with this Barcode"), "red"); this.clean_up(); + this.play_fail_sound(); reject(); return; } me.update_table(data).then(row => { - row ? resolve(row) : reject(); + this.play_success_sound(); + resolve(row); + }).catch(() => { + this.play_fail_sound(); + reject(); }); }); }); } update_table(data) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; const {item_code, barcode, batch_no, serial_no, uom} = data; @@ -77,6 +87,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { if (this.dont_allow_new_row) { this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); this.clean_up(); + reject(); return; } @@ -88,6 +99,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { if (this.is_duplicate_serial_no(row, serial_no)) { this.clean_up(); + reject(); return; } @@ -219,6 +231,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return this.items_table.find((d) => !d.item_code); } + play_success_sound() { + this.success_sound && frappe.utils.play_sound(this.success_sound); + } + + play_fail_sound() { + this.fail_sound && frappe.utils.play_sound(this.fail_sound); + } + clean_up() { this.scan_barcode_field.set_value(""); refresh_field(this.items_table_name); diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index bb2f95dd173..c58a46ba513 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -402,7 +402,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -986,7 +985,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-06-11 20:35:32.635804", + "modified": "2022-09-16 17:44:43.221804", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 74c5c07e47b..ff269d0e684 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -544,7 +544,6 @@ "hide_days": 1, "hide_seconds": 1, "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1549,7 +1548,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-06-10 03:52:22.212953", + "modified": "2022-09-16 17:43:57.007441", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 09a9652cca6..25806d6ed86 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -880,6 +880,9 @@ def get_events(start, end, filters=None): @frappe.whitelist() def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): """Creates Purchase Order for each Supplier. Returns a list of doc objects.""" + + from erpnext.setup.utils import get_exchange_rate + if not selected_items: return @@ -888,6 +891,15 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t def set_missing_values(source, target): target.supplier = supplier + target.currency = frappe.db.get_value( + "Supplier", filters={"name": supplier}, fieldname=["default_currency"] + ) + company_currency = frappe.db.get_value( + "Company", filters={"name": target.company}, fieldname=["default_currency"] + ) + + target.conversion_rate = get_exchange_rate(target.currency, company_currency, args="for_buying") + target.apply_discount_on = "" target.additional_discount_percentage = 0.0 target.discount_amount = 0.0 diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index dc698886a01..490504a7c9d 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -389,6 +389,7 @@ class Company(NestedSet): "capital_work_in_progress_account": "Capital Work in Progress", "asset_received_but_not_billed": "Asset Received But Not Billed", "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation", + "default_expense_account": "Cost of Goods Sold", } if self.enable_perpetual_inventory: @@ -398,7 +399,6 @@ class Company(NestedSet): "default_inventory_account": "Stock", "stock_adjustment_account": "Stock Adjustment", "expenses_included_in_valuation": "Expenses Included In Valuation", - "default_expense_account": "Cost of Goods Sold", } ) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index f9e934921d8..a8f907ed711 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -490,7 +490,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1336,7 +1335,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2022-06-10 03:52:04.197415", + "modified": "2022-09-16 17:46:17.701904", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index a70415dfc36..acaac920c9e 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -405,7 +405,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1158,7 +1157,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:43:40.664382", + "modified": "2022-09-16 17:45:58.430132", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b77c3a51348..62697244bab 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1241,6 +1241,37 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(query[0].value, 0) + def test_batch_expiry_for_purchase_receipt(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_receipt( + qty=1, + item_code=item.name, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index a952a93ac72..266ea5f674f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -625,6 +625,12 @@ frappe.ui.form.on('Stock Entry', { purchase_order: (frm) => { if (frm.doc.purchase_order) { frm.set_value("subcontracting_order", ""); + erpnext.utils.map_current_doc({ + method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontract_order', + source_name: frm.doc.purchase_order, + target_doc: frm, + freeze: true, + }); } }, @@ -632,7 +638,7 @@ frappe.ui.form.on('Stock Entry', { if (frm.doc.subcontracting_order) { frm.set_value("purchase_order", ""); erpnext.utils.map_current_doc({ - method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order', + method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontract_order', source_name: frm.doc.subcontracting_order, target_doc: frm, freeze: true, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 76bba8af646..738ac330e39 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -919,6 +919,16 @@ class StockEntry(StockController): ) if order_rm_detail: se_item.db_set(self.subcontract_data.rm_detail_field, order_rm_detail) + else: + if not se_item.allow_alternative_item: + frappe.throw( + _("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format( + se_item.idx, + se_item.item_code, + self.subcontract_data.order_doctype, + self.get(self.subcontract_data.order_field), + ) + ) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: @@ -1935,6 +1945,8 @@ class StockEntry(StockController): se_child.is_finished_item = item_row.get("is_finished_item", 0) se_child.is_scrap_item = item_row.get("is_scrap_item", 0) se_child.is_process_loss = item_row.get("is_process_loss", 0) + se_child.po_detail = item_row.get("po_detail") + se_child.sco_rm_detail = item_row.get("sco_rm_detail") for field in [ self.subcontract_data.rm_detail_field, @@ -2581,49 +2593,15 @@ def get_supplied_items( @frappe.whitelist() -def get_items_from_subcontracting_order(source_name, target_doc=None): - def post_process(source, target): - target.stock_entry_type = target.purpose = "Send to Subcontractor" - target.subcontracting_order = source_name +def get_items_from_subcontract_order(source_name, target_doc=None): + from erpnext.controllers.subcontracting_controller import make_rm_stock_entry - if target.items: - target.items = [] + if isinstance(target_doc, str): + target_doc = frappe.get_doc(json.loads(target_doc)) - warehouses = {} - for item in source.items: - warehouses[item.name] = item.warehouse - - for item in source.supplied_items: - target.append( - "items", - { - "s_warehouse": warehouses.get(item.reference_name), - "t_warehouse": source.supplier_warehouse, - "subcontracted_item": item.main_item_code, - "item_code": item.rm_item_code, - "qty": max(item.required_qty - item.total_supplied_qty, 0), - "transfer_qty": item.required_qty, - "uom": item.stock_uom, - "stock_uom": item.stock_uom, - "conversion_factor": 1, - }, - ) - - target_doc = get_mapped_doc( - "Subcontracting Order", - source_name, - { - "Subcontracting Order": { - "doctype": "Stock Entry", - "field_no_map": ["purchase_order"], - "validation": { - "docstatus": ["=", 1], - }, - }, - }, - target_doc, - post_process, - ignore_child_tables=True, + order_doctype = "Purchase Order" if target_doc.purchase_order else "Subcontracting Order" + target_doc = make_rm_stock_entry( + subcontract_order=source_name, order_doctype=order_doctype, target_doc=target_doc ) return target_doc diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 329cd7da09b..f7f8cbe4ee0 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -153,6 +153,9 @@ class StockLedgerEntry(Document): def validate_batch(self): if self.batch_no and self.voucher_type != "Stock Entry": + if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0: + return + expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 4e76ae781f9..7b984d38475 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -729,15 +729,19 @@ def create_stock_reconciliation(**args): sr.expense_account = args.expense_account or ( ( frappe.get_cached_value("Company", sr.company, "stock_adjustment_account") - or "Stock Adjustment - _TC" + or frappe.get_cached_value( + "Account", {"account_type": "Stock Adjustment", "company": sr.company}, "name" + ) ) if frappe.get_all("Stock Ledger Entry", {"company": sr.company}) - else "Temporary Opening - _TC" + else frappe.get_cached_value( + "Account", {"account_type": "Temporary", "company": sr.company}, "name" + ) ) sr.cost_center = ( args.cost_center or frappe.get_cached_value("Company", sr.company, "cost_center") - or "_Test Cost Center - _TC" + or frappe.get_cached_value("Cost Center", filters={"is_group": 0, "company": sr.company}) ) sr.append( diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py index 7a1b8c0cee9..0ec4e1ce957 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -45,4 +45,5 @@ def get_chart_data(data, filters): "datasets": [{"name": _("Total Delivered Amount"), "values": datapoints}], }, "type": "bar", + "fieldtype": "Currency", } diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index 23e3c8a97f5..df01b14d11a 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -4,6 +4,8 @@ import frappe from frappe import _ +from frappe.query_builder import Field +from frappe.query_builder.functions import Min, Timestamp from frappe.utils import add_days, getdate, today import erpnext @@ -28,7 +30,7 @@ def execute(filters=None): def get_unsync_date(filters): date = filters.from_date if not date: - date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""") + date = (frappe.qb.from_("Stock Ledger Entry").select(Min(Field("posting_date")))).run() date = date[0][0] if not date: @@ -54,22 +56,27 @@ def get_data(report_filters): result = [] voucher_wise_dict = {} - data = frappe.db.sql( - """ - SELECT - name, posting_date, posting_time, voucher_type, voucher_no, - stock_value_difference, stock_value, warehouse, item_code - FROM - `tabStock Ledger Entry` - WHERE - posting_date - = %s and company = %s - and is_cancelled = 0 - ORDER BY timestamp(posting_date, posting_time) asc, creation asc - """, - (from_date, report_filters.company), - as_dict=1, - ) + sle = frappe.qb.DocType("Stock Ledger Entry") + data = ( + frappe.qb.from_(sle) + .select( + sle.name, + sle.posting_date, + sle.posting_time, + sle.voucher_type, + sle.voucher_no, + sle.stock_value_difference, + sle.stock_value, + sle.warehouse, + sle.item_code, + ) + .where( + (sle.posting_date == from_date) + & (sle.company == report_filters.company) + & (sle.is_cancelled == 0) + ) + .orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation) + ).run(as_dict=True) for d in data: voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py index 15218e63a87..1b07f596c7b 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.py +++ b/erpnext/stock/report/item_price_stock/item_price_stock.py @@ -62,22 +62,28 @@ def get_data(filters, columns): def get_item_price_qty_data(filters): - conditions = "" - if filters.get("item_code"): - conditions += "where a.item_code=%(item_code)s" + item_price = frappe.qb.DocType("Item Price") + bin = frappe.qb.DocType("Bin") - item_results = frappe.db.sql( - """select a.item_code, a.item_name, a.name as price_list_name, - a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty - from `tabItem Price` a left join `tabBin` b - ON a.item_code = b.item_code - {conditions}""".format( - conditions=conditions - ), - filters, - as_dict=1, + query = ( + frappe.qb.from_(item_price) + .left_join(bin) + .on(item_price.item_code == bin.item_code) + .select( + item_price.item_code, + item_price.item_name, + item_price.name.as_("price_list_name"), + item_price.brand.as_("brand"), + bin.warehouse.as_("warehouse"), + bin.actual_qty.as_("actual_qty"), + ) ) + if filters.get("item_code"): + query = query.where(item_price.item_code == filters.get("item_code")) + + item_results = query.run(as_dict=True) + price_list_names = list(set(item.price_list_name for item in item_results)) buying_price_map = get_price_map(price_list_names, buying=1) diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py index 03a3a6a0b83..9fafe91c3f9 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -8,8 +8,7 @@ from frappe import _ def execute(filters=None): columns = get_columns() - conditions = get_conditions(filters) - data = get_data(conditions, filters) + data = get_data(filters) if not data: return [], [], None, [] @@ -19,49 +18,39 @@ def execute(filters=None): return columns, data, None, chart_data -def get_conditions(filters): - conditions = "" +def get_data(filters): + bin = frappe.qb.DocType("Bin") + wh = frappe.qb.DocType("Warehouse") + item = frappe.qb.DocType("Item") - if filters.get("warehouse"): - conditions += "AND warehouse in %(warehouse)s" - if filters.get("company"): - conditions += "AND company = %(company)s" - - return conditions - - -def get_data(conditions, filters): - data = frappe.db.sql( - """ - SELECT + query = ( + frappe.qb.from_(bin) + .from_(wh) + .from_(item) + .select( bin.warehouse, bin.item_code, - bin.actual_qty , - bin.ordered_qty , - bin.planned_qty , - bin.reserved_qty , + bin.actual_qty, + bin.ordered_qty, + bin.planned_qty, + bin.reserved_qty, bin.reserved_qty_for_production, - bin.projected_qty , - warehouse.company, - item.item_name , - item.description - FROM - `tabBin` bin, - `tabWarehouse` warehouse, - `tabItem` item - WHERE - bin.projected_qty<0 - AND warehouse.name = bin.warehouse - AND bin.item_code=item.name - {0} - ORDER BY bin.projected_qty;""".format( - conditions - ), - filters, - as_dict=1, + bin.projected_qty, + wh.company, + item.item_name, + item.description, + ) + .where((bin.projected_qty < 0) & (wh.name == bin.warehouse) & (bin.item_code == item.name)) + .orderby(bin.projected_qty) ) - return data + if filters.get("warehouse"): + query = query.where(bin.warehouse.isin(filters.get("warehouse"))) + + if filters.get("company"): + query = query.where(wh.company == filters.get("company")) + + return query.run(as_dict=True) def get_chart_data(data): diff --git a/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py b/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py new file mode 100644 index 00000000000..5884c32acc7 --- /dev/null +++ b/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py @@ -0,0 +1,51 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.report.item_shortage_report.item_shortage_report import ( + execute as item_shortage_report, +) + + +class TestItemShortageReport(FrappeTestCase): + def test_item_shortage_report(self): + item = make_item().name + so = make_sales_order(item_code=item) + + reserved_qty, projected_qty = frappe.db.get_value( + "Bin", + { + "item_code": item, + "warehouse": so.items[0].warehouse, + }, + ["reserved_qty", "projected_qty"], + ) + self.assertEqual(reserved_qty, so.items[0].qty) + self.assertEqual(projected_qty, -(so.items[0].qty)) + + filters = { + "company": so.company, + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertIn(item, item_code_list) + + filters = { + "company": so.company, + "warehouse": [so.items[0].warehouse], + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertIn(item, item_code_list) + + filters = { + "company": so.company, + "warehouse": ["Work In Progress - _TC"], + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertNotIn(item, item_code_list) diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index fe2d55a3913..b62a6ee6fd8 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -46,4 +46,5 @@ def get_chart_data(data, filters): }, "type": "bar", "colors": ["#5e64ff"], + "fieldtype": "Currency", } diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 40963f86373..15a2ac90912 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -205,20 +205,10 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll } make_stock_entry() { - frappe.model.open_mapped_doc({ - method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order', - source_name: cur_frm.doc.name, - freeze: true, - freeze_message: __('Creating Stock Entry ...') - }); - } - - make_rm_stock_entry(rm_items) { frappe.call({ method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry', args: { subcontract_order: cur_frm.doc.name, - rm_items: rm_items, order_doctype: cur_frm.doc.doctype }, callback: (r) => { diff --git a/erpnext/www/lms/__init__.py b/erpnext/www/lms/__init__.py new file mode 100644 index 00000000000..e69de29bb2d