Merge branch 'develop' of https://github.com/frappe/erpnext into fix-remove-no-copy-for-ignore_pricing_rule

This commit is contained in:
Deepesh Garg 2022-09-20 14:58:48 +05:30
commit d4b01f6ffd
31 changed files with 654 additions and 398 deletions

View File

@ -184,7 +184,9 @@ class JournalEntry(AccountsController):
}
)
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
inv, self.tax_withholding_category
)
if not tax_withholding_details:
return

View File

@ -1091,7 +1091,7 @@ frappe.ui.form.on('Payment Entry', {
$.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount;
});
},
@ -1182,7 +1182,7 @@ frappe.ui.form.on('Payment Entry', {
}
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction))
});
},
@ -1214,6 +1214,7 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.total_taxes_and_charges = 0.0;
frm.doc.base_total_taxes_and_charges = 0.0;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
let actual_tax_dict = {};
// maintain actual tax rate based on idx
@ -1234,8 +1235,8 @@ frappe.ui.form.on('Payment Entry', {
}
}
tax.tax_amount = current_tax_amount;
tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
// tax accounts are only in company currency
tax.base_tax_amount = current_tax_amount;
current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
if(i==0) {
@ -1244,9 +1245,29 @@ frappe.ui.form.on('Payment Entry', {
tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
}
tax.base_total = tax.total * frm.doc.source_exchange_rate;
frm.doc.total_taxes_and_charges += current_tax_amount;
frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
// tac accounts are only in company currency
tax.base_total = tax.total
// calculate total taxes and base total taxes
if(frm.doc.payment_type == "Pay") {
// tax accounts only have company currency
if(tax.currency != frm.doc.paid_to_account_currency) {
//total_taxes_and_charges has the target currency. so using target conversion rate
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate);
} else {
frm.doc.total_taxes_and_charges += current_tax_amount;
}
} else if(frm.doc.payment_type == "Receive") {
if(tax.currency != frm.doc.paid_from_account_currency) {
//total_taxes_and_charges has the target currency. so using source conversion rate
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate);
} else {
frm.doc.total_taxes_and_charges += current_tax_amount;
}
}
frm.doc.base_total_taxes_and_charges += tax.base_tax_amount;
frm.refresh_field('taxes');
frm.refresh_field('total_taxes_and_charges');

View File

@ -940,6 +940,13 @@ class PaymentEntry(AccountsController):
)
if not d.included_in_paid_amount:
if get_account_currency(payment_account) != self.company_currency:
if self.payment_type == "Receive":
exchange_rate = self.target_exchange_rate
elif self.payment_type in ["Pay", "Internal Transfer"]:
exchange_rate = self.source_exchange_rate
base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount"))
gl_entries.append(
self.get_gl_dict(
{
@ -1033,7 +1040,7 @@ class PaymentEntry(AccountsController):
for fieldname in tax_fields:
tax.set(fieldname, 0.0)
self.paid_amount_after_tax = self.paid_amount
self.paid_amount_after_tax = self.base_paid_amount
def determine_exclusive_rate(self):
if not any(cint(tax.included_in_paid_amount) for tax in self.get("taxes")):
@ -1052,7 +1059,7 @@ class PaymentEntry(AccountsController):
cumulated_tax_fraction += tax.tax_fraction_for_current_item
self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction))
self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction))
def calculate_taxes(self):
self.total_taxes_and_charges = 0.0
@ -1075,7 +1082,7 @@ class PaymentEntry(AccountsController):
current_tax_amount += actual_tax_dict[tax.idx]
tax.tax_amount = current_tax_amount
tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
tax.base_tax_amount = current_tax_amount
if tax.add_deduct_tax == "Deduct":
current_tax_amount *= -1.0
@ -1089,14 +1096,20 @@ class PaymentEntry(AccountsController):
self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax)
)
tax.base_total = tax.total * self.source_exchange_rate
tax.base_total = tax.total
if self.payment_type == "Pay":
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
if tax.currency != self.paid_to_account_currency:
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.total_taxes_and_charges += current_tax_amount
elif self.payment_type == "Receive":
if tax.currency != self.paid_from_account_currency:
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
else:
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += tax.base_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total

View File

@ -4,6 +4,7 @@
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
@ -722,6 +723,46 @@ class TestPaymentEntry(FrappeTestCase):
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
)
def test_gl_of_multi_currency_payment_with_taxes(self):
payment_entry = create_payment_entry(
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
)
payment_entry.append(
"taxes",
{
"account_head": "_Test Account Service Tax - _TC",
"charge_type": "Actual",
"tax_amount": 100,
"add_deduct_tax": "Add",
"description": "Test",
},
)
payment_entry.target_exchange_rate = 80
payment_entry.received_amount = 12.5
payment_entry = payment_entry.submit()
gle = qb.DocType("GL Entry")
gl_entries = (
qb.from_(gle)
.select(
gle.account,
gle.debit,
gle.credit,
gle.debit_in_account_currency,
gle.credit_in_account_currency,
)
.orderby(gle.account)
.where(gle.voucher_no == payment_entry.name)
.run()
)
expected_gl_entries = (
("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0),
("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0),
("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0),
)
self.assertEqual(gl_entries, expected_gl_entries)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()

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

@ -176,7 +176,7 @@
},
{
"collapsible": 1,
"depends_on": "eval:doc.apply_on != 'Transaction'",
"depends_on": "eval:doc.apply_on != 'Transaction' && !doc.mixed_conditions",
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Discount on Other Item"
@ -297,12 +297,12 @@
{
"fieldname": "min_qty",
"fieldtype": "Float",
"label": "Min Qty"
"label": "Min Qty (As Per Stock UOM)"
},
{
"fieldname": "max_qty",
"fieldtype": "Float",
"label": "Max Qty"
"label": "Max Qty (As Per Stock UOM)"
},
{
"fieldname": "column_break_21",
@ -481,7 +481,7 @@
"description": "System will notify to increase or decrease quantity or amount ",
"fieldname": "threshold_percentage",
"fieldtype": "Percent",
"label": "Threshold for Suggestion"
"label": "Threshold for Suggestion (In Percentage)"
},
{
"description": "Higher the number, higher the priority",
@ -583,10 +583,11 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2021-08-06 15:10:04.219321",
"modified": "2022-09-16 16:00:38.356266",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@ -642,5 +643,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@ -324,7 +324,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if isinstance(pricing_rule, str):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
if pricing_rule.get("suggestion"):
continue
@ -337,7 +337,6 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other:
item_details.update(
{
"apply_rule_on_other_items": json.dumps(pricing_rule.apply_rule_on_other_items),
"price_or_product_discount": pricing_rule.price_or_product_discount,
"apply_rule_on": (
frappe.scrub(pricing_rule.apply_rule_on_other)
@ -347,6 +346,9 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
}
)
if pricing_rule.apply_rule_on_other_items:
item_details["apply_rule_on_other_items"] = json.dumps(pricing_rule.apply_rule_on_other_items)
if pricing_rule.coupon_code_based == 1 and args.coupon_code == None:
return item_details
@ -492,7 +494,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
)
if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"):
items = get_pricing_rule_items(pricing_rule)
items = get_pricing_rule_items(pricing_rule, other_items=True)
item_details.apply_on = (
frappe.scrub(pricing_rule.apply_rule_on_other)
if pricing_rule.apply_rule_on_other

View File

@ -766,6 +766,68 @@ class TestPricingRule(unittest.TestCase):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
def test_pricing_rule_for_other_items_cond_with_amount(self):
item = make_item("Water Flask New")
other_item = make_item("Other Water Flask New")
make_item_price(item.name, "_Test Price List", 100)
make_item_price(other_item.name, "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
"apply_rule_on_other": "Item Code",
"price_or_product_discount": "Price",
"rate_or_discount": "Discount Percentage",
"other_item_code": other_item.name,
"items": [
{
"item_code": item.name,
}
],
"selling": 1,
"currency": "INR",
"min_amt": 200,
"discount_percentage": 10,
"company": "_Test Company",
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(do_not_save=True, item_code=item.name)
si.append(
"items",
{
"item_code": other_item.name,
"item_name": other_item.item_name,
"description": other_item.description,
"stock_uom": other_item.stock_uom,
"uom": other_item.stock_uom,
"cost_center": si.items[0].cost_center,
"expense_account": si.items[0].expense_account,
"warehouse": si.items[0].warehouse,
"conversion_factor": 1,
"qty": 1,
},
)
si.selling_price_list = "_Test Price List"
si.save()
self.assertEqual(si.items[0].discount_percentage, 0)
self.assertEqual(si.items[1].discount_percentage, 0)
si.items[0].qty = 2
si.save()
self.assertEqual(si.items[0].discount_percentage, 0)
self.assertEqual(si.items[0].stock_qty, 2)
self.assertEqual(si.items[0].amount, 200)
self.assertEqual(si.items[0].price_list_rate, 100)
self.assertEqual(si.items[1].discount_percentage, 10)
si.delete()
rule.delete()
test_dependencies = ["Campaign"]

View File

@ -252,12 +252,6 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
stock_qty = flt(args.get("stock_qty"))
amount = flt(args.get("price_list_rate")) * flt(args.get("qty"))
if pricing_rules[0].apply_rule_on_other:
field = frappe.scrub(pricing_rules[0].apply_rule_on_other)
if field and pricing_rules[0].get("other_" + field) != args.get(field):
return
pr_doc = frappe.get_cached_doc("Pricing Rule", pricing_rules[0].name)
if pricing_rules[0].mixed_conditions and doc:
@ -274,7 +268,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
amount += data[1]
if pricing_rules[0].apply_rule_on_other and not pricing_rules[0].mixed_conditions and doc:
pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules) or []
pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, args) or []
else:
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, args)
@ -352,16 +346,14 @@ def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, tr
if fieldname:
msg = _(
"If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item."
).format(
type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description)
)
).format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.title))
if fieldname in ["min_amt", "max_amt"]:
msg = _("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.").format(
type_of_transaction,
fmt_money(args.get(fieldname), currency=args.get("currency")),
bold(item_code),
bold(args.rule_description),
bold(args.title),
)
frappe.msgprint(msg)
@ -454,17 +446,29 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
return sum_qty, sum_amt, items
def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules):
items = get_pricing_rule_items(pr_doc)
def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
other_items = get_pricing_rule_items(pr_doc, other_items=True)
pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on"))
apply_on = frappe.scrub(pr_doc.get("apply_on"))
items = []
for d in pr_doc.get(pricing_rule_apply_on):
if apply_on == "item_group":
items.extend(get_child_item_groups(d.get(apply_on)))
else:
items.append(d.get(apply_on))
for row in doc.items:
if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items:
pricing_rules = filter_pricing_rules_for_qty_amount(
row.get("stock_qty"), row.get("amount"), pricing_rules, row
)
if row.get(apply_on) in items:
if not row.get("qty"):
continue
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
if pricing_rules and pricing_rules[0]:
pricing_rules[0].apply_rule_on_other_items = items
pricing_rules[0].apply_rule_on_other_items = other_items
return pricing_rules
@ -658,21 +662,21 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values
doc.append("items", args)
def get_pricing_rule_items(pr_doc):
def get_pricing_rule_items(pr_doc, other_items=False) -> list:
apply_on_data = []
apply_on = frappe.scrub(pr_doc.get("apply_on"))
pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on"))
for d in pr_doc.get(pricing_rule_apply_on):
if apply_on == "item_group":
apply_on_data.extend(get_child_item_groups(d.get(apply_on)))
else:
apply_on_data.append(d.get(apply_on))
if pr_doc.apply_rule_on_other:
if pr_doc.apply_rule_on_other and other_items:
apply_on = frappe.scrub(pr_doc.apply_rule_on_other)
apply_on_data.append(pr_doc.get("other_" + apply_on))
else:
for d in pr_doc.get(pricing_rule_apply_on):
if apply_on == "item_group":
apply_on_data.extend(get_child_item_groups(d.get(apply_on)))
else:
apply_on_data.append(d.get(apply_on))
return list(set(apply_on_data))

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",
@ -1366,7 +1368,7 @@
"width": "50px"
},
{
"depends_on": "eval:doc.is_subcontracted",
"depends_on": "eval:doc.is_subcontracted",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
@ -1425,13 +1427,25 @@
"hidden": 1,
"label": "Is Old Subcontracting Flow",
"read_only": 1
}
},
{
"fieldname": "tax_withheld_vouchers_section",
"fieldtype": "Section Break",
"label": "Tax Withheld Vouchers"
},
{
"fieldname": "tax_withheld_vouchers",
"fieldtype": "Table",
"label": "Tax Withheld Vouchers",
"options": "Tax Withheld Vouchers",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2022-09-16 17:45:25.345996",
"modified": "2022-09-13 23:39:54.525037",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -1519,7 +1519,7 @@ class PurchaseInvoice(BuyingController):
if not self.tax_withholding_category:
return
tax_withholding_details, advance_taxes = get_party_tax_withholding_details(
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
self, self.tax_withholding_category
)
@ -1548,6 +1548,19 @@ class PurchaseInvoice(BuyingController):
for d in to_remove:
self.remove(d)
## Add pending vouchers on which tax was withheld
self.set("tax_withheld_vouchers", [])
for voucher_no, voucher_details in voucher_wise_amount.items():
self.append(
"tax_withheld_vouchers",
{
"voucher_name": voucher_no,
"voucher_type": voucher_details.get("voucher_type"),
"taxable_amount": voucher_details.get("amount"),
},
)
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()

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

@ -109,7 +109,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
).format(tax_withholding_category, inv.company, party)
)
tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount(
party_type, parties, inv, tax_details, posting_date, pan_no
)
@ -119,7 +119,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances
return tax_row, tax_deducted_on_advances, voucher_wise_amount
else:
return tax_row
@ -217,7 +217,9 @@ def get_lower_deduction_certificate(tax_details, pan_no):
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
vouchers, voucher_wise_amount = get_invoice_vouchers(
parties, tax_details, inv.company, party_type=party_type
)
advance_vouchers = get_advance_vouchers(
parties,
company=inv.company,
@ -236,6 +238,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
tax_amount = 0
if party_type == "Supplier":
ldc = get_lower_deduction_certificate(tax_details, pan_no)
if tax_deducted:
@ -261,12 +264,13 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
return tax_amount, tax_deducted, tax_deducted_on_advances
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
dr_or_cr = "credit" if party_type == "Supplier" else "debit"
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
voucher_wise_amount = {}
vouchers = []
filters = {
"company": company,
@ -281,29 +285,40 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
)
invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"])
journal_entries = frappe.db.sql(
for d in invoices_details:
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
journal_entries_details = frappe.db.sql(
"""
SELECT j.name
SELECT j.name, ja.credit - ja.debit AS amount
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
WHERE
j.docstatus = 1
j.name = ja.parent
AND j.docstatus = 1
AND j.is_opening = 'No'
AND j.posting_date between %s and %s
AND ja.{dr_or_cr} > 0
AND ja.party in %s
""".format(
dr_or_cr=dr_or_cr
AND j.apply_tds = 1
AND j.tax_withholding_category = %s
""",
(
tax_details.from_date,
tax_details.to_date,
tuple(parties),
tax_details.get("tax_withholding_category"),
),
(tax_details.from_date, tax_details.to_date, tuple(parties)),
as_list=1,
as_dict=1,
)
if journal_entries:
journal_entries = journal_entries[0]
if journal_entries_details:
for d in journal_entries_details:
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
return invoices + journal_entries
return vouchers, voucher_wise_amount
def get_advance_vouchers(
@ -329,23 +344,25 @@ def get_advance_vouchers(
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
advances = [d.reference_name for d in inv.get("advances")]
tax_info = []
if advances:
pe = frappe.qb.DocType("Payment Entry").as_("pe")
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
if inv.get("advances"):
advances = [d.reference_name for d in inv.get("advances")]
tax_info = (
frappe.qb.from_(at)
.inner_join(pe)
.on(pe.name == at.parent)
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
.where(at.parent.isin(advances))
.where(at.account_head == tax_details.account_head)
.run(as_dict=True)
)
if advances:
pe = frappe.qb.DocType("Payment Entry").as_("pe")
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
tax_info = (
frappe.qb.from_(at)
.inner_join(pe)
.on(pe.name == at.parent)
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
.where(at.parent.isin(advances))
.where(at.account_head == tax_details.account_head)
.run(as_dict=True)
)
return tax_info
@ -394,11 +411,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.net_total
debit_note_amount = get_debit_note_amount(
parties, tax_details.from_date, tax_details.to_date, inv.company
)
supp_credit_amt -= debit_note_amount
threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
@ -515,22 +527,6 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
return tds_amount
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
filters = {
"supplier": ["in", suppliers],
"is_return": 1,
"docstatus": 1,
"posting_date": ["between", (from_date, to_date)],
}
fields = ["abs(sum(net_total)) as net_total"]
if company:
filters["company"] = company
return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount):
return current_amount * rate / 100

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

@ -239,8 +239,10 @@ frappe.ui.form.on('Asset', {
item_code: function(frm) {
if(frm.doc.item_code) {
if(frm.doc.item_code && frm.doc.calculate_depreciation) {
frm.trigger('set_finance_book');
} else {
frm.set_value('finance_books', []);
}
},
@ -381,6 +383,11 @@ frappe.ui.form.on('Asset', {
calculate_depreciation: function(frm) {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
if (frm.doc.item_code && frm.doc.calculate_depreciation ) {
frm.trigger("set_finance_book");
} else {
frm.set_value("finance_books", []);
}
},
gross_purchase_amount: function(frm) {

View File

@ -295,131 +295,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}
make_stock_entry() {
var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; });
var me = this;
if(items.length >= 1){
me.raw_material_data = [];
me.show_dialog = 1;
let title = __('Transfer Material to Supplier');
let fields = [
{fieldtype:'Section Break', label: __('Raw Materials')},
{fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'),
fields: [
{
fieldtype:'Data',
fieldname:'item_code',
label: __('Item'),
read_only:1,
in_list_view:1
},
{
fieldtype:'Data',
fieldname:'rm_item_code',
label: __('Raw Material'),
read_only:1,
in_list_view:1
},
{
fieldtype:'Float',
read_only:1,
fieldname:'qty',
label: __('Quantity'),
read_only:1,
in_list_view:1
},
{
fieldtype:'Data',
read_only:1,
fieldname:'warehouse',
label: __('Reserve Warehouse'),
in_list_view:1
},
{
fieldtype:'Float',
read_only:1,
fieldname:'rate',
label: __('Rate'),
hidden:1
},
{
fieldtype:'Float',
read_only:1,
fieldname:'amount',
label: __('Amount'),
hidden:1
},
{
fieldtype:'Link',
read_only:1,
fieldname:'uom',
label: __('UOM'),
hidden:1
}
],
data: me.raw_material_data,
get_data: function() {
return me.raw_material_data;
}
}
]
me.dialog = new frappe.ui.Dialog({
title: title, fields: fields
});
if (me.frm.doc['supplied_items']) {
me.frm.doc['supplied_items'].forEach((item, index) => {
if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) {
me.raw_material_data.push ({
'name':item.name,
'item_code': item.main_item_code,
'rm_item_code': item.rm_item_code,
'item_name': item.rm_item_code,
'qty': item.required_qty - item.supplied_qty,
'warehouse':item.reserve_warehouse,
'rate':item.rate,
'amount':item.amount,
'stock_uom':item.stock_uom
});
me.dialog.fields_dict.sub_con_rm_items.grid.refresh();
}
})
}
me.dialog.get_field('sub_con_rm_items').check_all_rows()
me.dialog.show()
this.dialog.set_primary_action(__('Transfer'), function() {
me.values = me.dialog.get_values();
if(me.values) {
me.values.sub_con_rm_items.map((row,i) => {
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
let row_id = i+1;
frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id]));
}
})
me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children())
me.dialog.hide()
}
});
}
me.dialog.get_close_btn().on('click', () => {
me.dialog.hide();
});
}
_make_rm_stock_entry(rm_items) {
frappe.call({
method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry",
args: {
subcontract_order: cur_frm.doc.name,
rm_items: rm_items,
order_doctype: cur_frm.doc.doctype
}
,
},
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);

View File

@ -572,6 +572,11 @@ class AccountsController(TransactionBase):
# if user changed the discount percentage then set user's discount percentage ?
if pricing_rule_args.get("price_or_product_discount") == "Price":
item.set("pricing_rules", pricing_rule_args.get("pricing_rules"))
if pricing_rule_args.get("apply_rule_on_other_items"):
other_items = json.loads(pricing_rule_args.get("apply_rule_on_other_items"))
if other_items and item.item_code not in other_items:
return
item.set("discount_percentage", pricing_rule_args.get("discount_percentage"))
item.set("discount_amount", pricing_rule_args.get("discount_amount"))
if pricing_rule_args.get("pricing_rule_for") == "Rate":

View File

@ -770,7 +770,7 @@ def get_item_details(items):
item = frappe.qb.DocType("Item")
item_list = (
frappe.qb.from_(item)
.select(item.item_code, item.description, item.allow_alternative_item)
.select(item.item_code, item.item_name, item.description, item.allow_alternative_item)
.where(item.name.isin(items))
.run(as_dict=True)
)
@ -783,68 +783,93 @@ def get_item_details(items):
@frappe.whitelist()
def make_rm_stock_entry(subcontract_order, rm_items, order_doctype="Subcontracting Order"):
rm_items_list = rm_items
if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items)
elif not rm_items:
frappe.throw(_("No Items available for transfer"))
if rm_items_list:
fg_items = list(set(item["item_code"] for item in rm_items_list))
else:
frappe.throw(_("No Items selected for transfer"))
def make_rm_stock_entry(
subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None
):
if subcontract_order:
subcontract_order = frappe.get_doc(order_doctype, subcontract_order)
if fg_items:
items = tuple(set(item["rm_item_code"] for item in rm_items_list))
item_wh = get_item_details(items)
if not rm_items:
if not subcontract_order.supplied_items:
frappe.throw(_("No item available for transfer."))
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
stock_entry.purchase_order = subcontract_order.name
else:
stock_entry.subcontracting_order = subcontract_order.name
stock_entry.supplier = subcontract_order.supplier
stock_entry.supplier_name = subcontract_order.supplier_name
stock_entry.supplier_address = subcontract_order.supplier_address
stock_entry.address_display = subcontract_order.address_display
stock_entry.company = subcontract_order.company
stock_entry.to_warehouse = subcontract_order.supplier_warehouse
stock_entry.set_stock_entry_type()
rm_items = subcontract_order.supplied_items
if order_doctype == "Purchase Order":
rm_detail_field = "po_detail"
else:
rm_detail_field = "sco_rm_detail"
fg_item_code_list = list(
set(item.get("main_item_code") or item.get("item_code") for item in rm_items)
)
for item_code in fg_items:
for rm_item_data in rm_items_list:
if rm_item_data["item_code"] == item_code:
rm_item_code = rm_item_data["rm_item_code"]
items_dict = {
rm_item_code: {
rm_detail_field: rm_item_data.get("name"),
"item_name": rm_item_data["item_name"],
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item_data["qty"],
"from_warehouse": rm_item_data["warehouse"],
"stock_uom": rm_item_data["stock_uom"],
"serial_no": rm_item_data.get("serial_no"),
"batch_no": rm_item_data.get("batch_no"),
"main_item_code": rm_item_data["item_code"],
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
if fg_item_code_list:
rm_item_code_list = tuple(set(item.get("rm_item_code") for item in rm_items))
item_wh = get_item_details(rm_item_code_list)
field_no_map, rm_detail_field = "purchase_order", "sco_rm_detail"
if order_doctype == "Purchase Order":
field_no_map, rm_detail_field = "subcontracting_order", "po_detail"
if target_doc and target_doc.get("items"):
target_doc.items = []
stock_entry = get_mapped_doc(
order_doctype,
subcontract_order.name,
{
order_doctype: {
"doctype": "Stock Entry",
"field_map": {
"to_warehouse": "supplier_warehouse",
},
"field_no_map": [field_no_map],
"validation": {
"docstatus": ["=", 1],
},
},
},
target_doc,
ignore_child_tables=True,
)
stock_entry.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
stock_entry.purchase_order = subcontract_order.name
else:
stock_entry.subcontracting_order = subcontract_order.name
stock_entry.set_stock_entry_type()
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if rm_item.get("main_item_code") or rm_item.get("item_code") == fg_item_code:
rm_item_code = rm_item.get("rm_item_code")
items_dict = {
rm_item_code: {
rm_detail_field: rm_item.get("name"),
"item_name": rm_item.get("item_name")
or item_wh.get(rm_item_code, {}).get("item_name", ""),
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item.get("qty")
or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0),
"from_warehouse": rm_item.get("warehouse") or rm_item.get("reserve_warehouse"),
"to_warehouse": subcontract_order.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"),
"serial_no": rm_item.get("serial_no"),
"batch_no": rm_item.get("batch_no"),
"main_item_code": fg_item_code,
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
}
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer"))
return subcontract_order.name
stock_entry.add_to_stock_entry_detail(items_dict)
if target_doc:
return stock_entry
else:
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer."))
def add_items_in_ste(

View File

@ -198,7 +198,9 @@ class ProductionPlan(Document):
so_item.parent,
so_item.item_code,
so_item.warehouse,
((so_item.qty - so_item.work_order_qty) * so_item.conversion_factor).as_("pending_qty"),
(
(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,
)

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
@ -610,15 +611,21 @@ class TestProductionPlan(FrappeTestCase):
"""
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
make_stock_entry(item_code="_Test Item", target="Work In Progress - _TC", qty=2, basic_rate=100)
make_stock_entry(
item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100
)
make_stock_entry(
item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100
item_code="_Test Item Home Desktop 100", target="Work In Progress - _TC", qty=4, basic_rate=100
)
item = "Test Production Item 1"
so = make_sales_order(item_code=item, qty=1)
item = "_Test FG Item"
make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=1)
so = make_sales_order(item_code=item, qty=2)
dn = make_delivery_note(so.name)
dn.items[0].qty = 1
dn.save()
dn.submit()
pln = create_production_plan(
company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True

View File

@ -152,6 +152,7 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.parentfield = parentfield;
new_child_doc.parenttype = doctype;
new_doc[parentfield] = [new_child_doc];
new_doc.project = frm.doc.name;
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});

View File

@ -1492,7 +1492,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.model.set_value(child.doctype, child.name, "rate", value);
}
if (key === "pricing_rules") {
frappe.model.set_value(child.doctype, child.name, key, value);
}
if (key !== "free_item_data") {
if (child.apply_rule_on_other_items && JSON.parse(child.apply_rule_on_other_items).length) {
if (!in_list(JSON.parse(child.apply_rule_on_other_items), child.item_code)) {
continue;
}
}
frappe.model.set_value(child.doctype, child.name, key, value);
}
}
@ -1510,11 +1520,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.remove_pricing_rule(frappe.get_doc(child.doctype, child.name));
}
if (child.free_item_data.length > 0) {
if (child.free_item_data && child.free_item_data.length > 0) {
this.apply_product_discount(child);
}
if (child.apply_rule_on_other_items) {
if (child.apply_rule_on_other_items && JSON.parse(child.apply_rule_on_other_items).length) {
items_rule_dict[child.name] = child;
}
}
@ -1530,11 +1540,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
for(var k in args) {
let data = args[k];
if (data && data.apply_rule_on_other_items) {
if (data && data.apply_rule_on_other_items && JSON.parse(data.apply_rule_on_other_items)) {
me.frm.doc.items.forEach(d => {
if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) {
if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on])) {
for(var k in data) {
if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'price' || k === 'pricing_rules')) {
if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'Price' || k === 'pricing_rules')) {
frappe.model.set_value(d.doctype, d.name, k, data[k]);
}
}

View File

@ -880,6 +880,9 @@ def get_events(start, end, filters=None):
@frappe.whitelist()
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
"""Creates Purchase Order for each Supplier. Returns a list of doc objects."""
from erpnext.setup.utils import get_exchange_rate
if not selected_items:
return
@ -888,6 +891,15 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
def set_missing_values(source, target):
target.supplier = supplier
target.currency = frappe.db.get_value(
"Supplier", filters={"name": supplier}, fieldname=["default_currency"]
)
company_currency = frappe.db.get_value(
"Company", filters={"name": target.company}, fieldname=["default_currency"]
)
target.conversion_rate = get_exchange_rate(target.currency, company_currency, args="for_buying")
target.apply_discount_on = ""
target.additional_discount_percentage = 0.0
target.discount_amount = 0.0

View File

@ -625,6 +625,12 @@ frappe.ui.form.on('Stock Entry', {
purchase_order: (frm) => {
if (frm.doc.purchase_order) {
frm.set_value("subcontracting_order", "");
erpnext.utils.map_current_doc({
method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontract_order',
source_name: frm.doc.purchase_order,
target_doc: frm,
freeze: true,
});
}
},
@ -632,7 +638,7 @@ frappe.ui.form.on('Stock Entry', {
if (frm.doc.subcontracting_order) {
frm.set_value("purchase_order", "");
erpnext.utils.map_current_doc({
method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order',
method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontract_order',
source_name: frm.doc.subcontracting_order,
target_doc: frm,
freeze: true,

View File

@ -919,6 +919,16 @@ class StockEntry(StockController):
)
if order_rm_detail:
se_item.db_set(self.subcontract_data.rm_detail_field, order_rm_detail)
else:
if not se_item.allow_alternative_item:
frappe.throw(
_("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format(
se_item.idx,
se_item.item_code,
self.subcontract_data.order_doctype,
self.get(self.subcontract_data.order_field),
)
)
elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
for row in self.items:
if not row.subcontracted_item:
@ -1935,6 +1945,8 @@ class StockEntry(StockController):
se_child.is_finished_item = item_row.get("is_finished_item", 0)
se_child.is_scrap_item = item_row.get("is_scrap_item", 0)
se_child.is_process_loss = item_row.get("is_process_loss", 0)
se_child.po_detail = item_row.get("po_detail")
se_child.sco_rm_detail = item_row.get("sco_rm_detail")
for field in [
self.subcontract_data.rm_detail_field,
@ -2581,49 +2593,15 @@ def get_supplied_items(
@frappe.whitelist()
def get_items_from_subcontracting_order(source_name, target_doc=None):
def post_process(source, target):
target.stock_entry_type = target.purpose = "Send to Subcontractor"
target.subcontracting_order = source_name
def get_items_from_subcontract_order(source_name, target_doc=None):
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
if target.items:
target.items = []
if isinstance(target_doc, str):
target_doc = frappe.get_doc(json.loads(target_doc))
warehouses = {}
for item in source.items:
warehouses[item.name] = item.warehouse
for item in source.supplied_items:
target.append(
"items",
{
"s_warehouse": warehouses.get(item.reference_name),
"t_warehouse": source.supplier_warehouse,
"subcontracted_item": item.main_item_code,
"item_code": item.rm_item_code,
"qty": max(item.required_qty - item.total_supplied_qty, 0),
"transfer_qty": item.required_qty,
"uom": item.stock_uom,
"stock_uom": item.stock_uom,
"conversion_factor": 1,
},
)
target_doc = get_mapped_doc(
"Subcontracting Order",
source_name,
{
"Subcontracting Order": {
"doctype": "Stock Entry",
"field_no_map": ["purchase_order"],
"validation": {
"docstatus": ["=", 1],
},
},
},
target_doc,
post_process,
ignore_child_tables=True,
order_doctype = "Purchase Order" if target_doc.purchase_order else "Subcontracting Order"
target_doc = make_rm_stock_entry(
subcontract_order=source_name, order_doctype=order_doctype, target_doc=target_doc
)
return target_doc

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

@ -205,20 +205,10 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
}
make_stock_entry() {
frappe.model.open_mapped_doc({
method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order',
source_name: cur_frm.doc.name,
freeze: true,
freeze_message: __('Creating Stock Entry ...')
});
}
make_rm_stock_entry(rm_items) {
frappe.call({
method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry',
args: {
subcontract_order: cur_frm.doc.name,
rm_items: rm_items,
order_doctype: cur_frm.doc.doctype
},
callback: (r) => {

View File