Merge pull request #32291 from frappe/version-13-hotfix

chore: release v13
This commit is contained in:
Deepesh Garg
2022-09-21 00:41:03 +05:30
committed by GitHub
27 changed files with 882 additions and 397 deletions

View File

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

View File

@@ -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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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