diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 8cc20038770..73e673ea38b 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -194,7 +194,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 3a89ce8cd12..6be0920d2a8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1111,7 +1111,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; }); }, @@ -1202,7 +1202,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)) }); }, @@ -1234,6 +1234,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 @@ -1254,8 +1255,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) { @@ -1264,9 +1265,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 b1976ebae65..904d7af757f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -944,6 +944,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( { @@ -1059,7 +1066,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"))): @@ -1078,7 +1085,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 @@ -1101,7 +1108,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 @@ -1115,14 +1122,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 a8211c81f1b..004c84c0221 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.utils import flt, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import ( @@ -743,6 +744,46 @@ class TestPaymentEntry(unittest.TestCase): 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_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/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index bd0116443ff..fc5b94284ef 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", @@ -1417,13 +1419,25 @@ "label": "Advance Tax", "options": "Advance Tax", "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": "2021-11-25 13:31:02.716727", + "modified": "2022-09-13 23:39:54.525037", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1483,6 +1497,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 75508be8d09..6476845de78 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1457,7 +1457,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 ) @@ -1486,6 +1486,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/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 3db21dc5a41..2d169f70aa0 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -100,7 +100,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 ) @@ -110,7 +110,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 @@ -208,7 +208,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, @@ -227,6 +229,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: @@ -252,12 +255,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, @@ -272,29 +276,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( @@ -320,23 +335,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 @@ -358,6 +375,9 @@ def get_deducted_tax(taxable_vouchers, tax_details): def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): tds_amount = 0 + supp_credit_amt = 0.0 + supp_jv_credit_amt = 0.0 + invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} field = "sum(net_total)" @@ -366,30 +386,25 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): invoice_filters.pop("apply_tds", None) field = "sum(grand_total)" - supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 + if vouchers: + supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 - supp_jv_credit_amt = ( - frappe.db.get_value( - "Journal Entry Account", - { - "parent": ("in", vouchers), - "docstatus": 1, - "party": ("in", parties), - "reference_type": ("!=", "Purchase Invoice"), - }, - "sum(credit_in_account_currency)", - ) - or 0.0 - ) + supp_jv_credit_amt = ( + frappe.db.get_value( + "Journal Entry Account", + { + "parent": ("in", vouchers), + "docstatus": 1, + "party": ("in", parties), + "reference_type": ("!=", "Purchase Invoice"), + }, + "sum(credit_in_account_currency)", + ) + ) or 0.0 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) @@ -422,36 +437,40 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): tcs_amount = 0 + invoiced_amt = 0 + advance_amt = 0 # sum of debit entries made from sales invoices - invoiced_amt = ( - frappe.db.get_value( - "GL Entry", - { - "is_cancelled": 0, - "party": ["in", parties], - "company": inv.company, - "voucher_no": ["in", vouchers], - }, - "sum(debit)", + if vouchers: + invoiced_amt = ( + frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "party": ["in", parties], + "company": inv.company, + "voucher_no": ["in", vouchers], + }, + "sum(debit)", + ) + or 0.0 ) - or 0.0 - ) # sum of credit entries made from PE / JV with unset 'against voucher' - advance_amt = ( - frappe.db.get_value( - "GL Entry", - { - "is_cancelled": 0, - "party": ["in", parties], - "company": inv.company, - "voucher_no": ["in", adv_vouchers], - }, - "sum(credit)", + if advance_amt: + advance_amt = ( + frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "party": ["in", parties], + "company": inv.company, + "voucher_no": ["in", adv_vouchers], + }, + "sum(credit)", + ) + or 0.0 ) - or 0.0 - ) # sum of credit entries made from sales invoice credit_note_amt = sum( @@ -506,22 +525,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/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index ac706666547..c987231fe17 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -97,6 +97,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row.update({"rate": d.base_net_rate, "amount": d.base_net_amount}) total_tax = 0 + total_other_charges = 0 for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update( @@ -105,10 +106,18 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), } ) - total_tax += flt(item_tax.get("tax_amount")) + if item_tax.get("is_other_charges"): + total_other_charges += flt(item_tax.get("tax_amount")) + else: + total_tax += flt(item_tax.get("tax_amount")) row.update( - {"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency} + { + "total_tax": total_tax, + "total_other_charges": total_other_charges, + "total": d.base_net_amount + total_tax, + "currency": company_currency, + } ) if filters.get("group_by"): @@ -477,7 +486,7 @@ def get_tax_accounts( tax_details = frappe.db.sql( """ select - name, parent, description, item_wise_tax_detail, + name, parent, description, item_wise_tax_detail, account_head, charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount from `tab%s` where @@ -493,11 +502,22 @@ def get_tax_accounts( tuple([doctype] + list(invoice_item_row)), ) + account_doctype = frappe.qb.DocType("Account") + + query = ( + frappe.qb.from_(account_doctype) + .select(account_doctype.name) + .where((account_doctype.account_type == "Tax")) + ) + + tax_accounts = query.run() + for ( name, parent, description, item_wise_tax_detail, + account_head, charge_type, add_deduct_tax, tax_amount, @@ -540,7 +560,11 @@ def get_tax_accounts( ) itemised_tax.setdefault(d.name, {})[description] = frappe._dict( - {"tax_rate": tax_rate, "tax_amount": tax_value} + { + "tax_rate": tax_rate, + "tax_amount": tax_value, + "is_other_charges": 0 if tuple([account_head]) in tax_accounts else 1, + } ) except ValueError: @@ -583,6 +607,13 @@ def get_tax_accounts( "options": "currency", "width": 100, }, + { + "label": _("Total Other Charges"), + "fieldname": "total_other_charges", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, { "label": _("Total"), "fieldname": "total", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 47de0eff359..c4fafba2669 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -818,6 +818,31 @@ def get_held_invoices(party_type, party): return held_invoices +def remove_return_pos_invoices(party_type, party, invoice_list): + if invoice_list: + + if party_type == "Customer": + sinv = frappe.qb.DocType("Sales Invoice") + return_pos = ( + frappe.qb.from_(sinv) + .select(sinv.name) + .where((sinv.is_pos == 1) & (sinv.docstatus == 1) & (sinv.is_return == 1)) + .run() + ) + + if return_pos: + return_pos = [x[0] for x in return_pos] + else: + return invoice_list + + # remove pos return invoices from invoice_list + for idx, inv in enumerate(invoice_list, 0): + if inv.voucher_no in return_pos: + del invoice_list[idx] + + return invoice_list + + def get_outstanding_invoices(party_type, party, account, condition=None, filters=None): outstanding_invoices = [] precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 @@ -868,6 +893,8 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters as_dict=True, ) + invoice_list = remove_return_pos_invoices(party_type, party, invoice_list) + payment_entries = frappe.db.sql( """ select against_voucher_type, against_voucher, diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 986c686e5b3..35ba16c74a7 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -22,9 +22,9 @@ def execute(filters=None): def get_columns(leave_types): columns = [ - _("Employee") + ":Link.Employee:150", + _("Employee") + ":Link/Employee:150", _("Employee Name") + "::200", - _("Department") + "::150", + _("Department") + ":Link/Department:150", ] for leave_type in leave_types: diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 19d0d84a46f..c17de234556 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -520,6 +520,8 @@ def get_accrued_interest_entries(against_loan, posting_date=None): if not posting_date: posting_date = getdate() + precision = cint(frappe.db.get_default("currency_precision")) or 2 + unpaid_accrued_entries = frappe.db.sql( """ SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount, @@ -540,6 +542,13 @@ def get_accrued_interest_entries(against_loan, posting_date=None): as_dict=1, ) + # Skip entries with zero interest amount & payable principal amount + unpaid_accrued_entries = [ + d + for d in unpaid_accrued_entries + if flt(d.interest_amount, precision) > 0 or flt(d.payable_principal_amount, precision) > 0 + ] + return unpaid_accrued_entries diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index b6567ec15d8..a0da7275eb7 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 six import iteritems from erpnext.manufacturing.doctype.bom.bom import get_children, 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() @@ -754,29 +810,45 @@ 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, + 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) @@ -801,33 +873,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: @@ -915,48 +1001,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 @@ -965,37 +1072,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 cbae275031e..f0d4ce0ca82 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 @@ -538,15 +539,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/patches.txt b/erpnext/patches.txt index cbc224f99e4..b3099644780 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -373,3 +373,4 @@ erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair +execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0}) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 31460f66ea3..9f1bdb0a11d 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -134,6 +134,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/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b673223dc6b..e9024aee69e 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -950,6 +950,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 @@ -958,6 +961,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/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fa7900bd3b2..174fef1084f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -879,6 +879,27 @@ class StockEntry(StockController): se_item.idx, se_item.item_code, total_allowed, self.purchase_order ) ) + elif not se_item.get("po_detail"): + filters = { + "parent": self.purchase_order, + "docstatus": 1, + "rm_item_code": se_item.item_code, + "main_item_code": se_item.subcontracted_item, + } + + order_rm_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name") + if order_rm_detail: + se_item.db_set("po_detail", 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, + "Purchase Order", + self.purchase_order, + ) + ) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: 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/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 1956238331e..7c430e491ab 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -34,6 +34,9 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) for item, item_dict in item_details.items(): + if not flt(item_dict.get("total_qty"), precision): + continue + earliest_age, latest_age = 0, 0 details = item_dict["details"]