mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-21 15:48:29 +00:00
Merge pull request #32291 from frappe/version-13-hotfix
chore: release v13
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user