Merge branch 'develop' into github-issue-33344
This commit is contained in:
commit
6e9d001819
@ -1485,11 +1485,17 @@ class PurchaseInvoice(BuyingController):
|
||||
if po_details:
|
||||
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
|
||||
|
||||
adjust_incoming_rate = frappe.db.get_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||
)
|
||||
|
||||
for pr in set(updated_pr):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
|
||||
|
||||
pr_doc = frappe.get_doc("Purchase Receipt", pr)
|
||||
update_billing_percentage(pr_doc, update_modified=update_modified)
|
||||
update_billing_percentage(
|
||||
pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate
|
||||
)
|
||||
|
||||
def get_pr_details_billed_amt(self):
|
||||
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
|
||||
|
@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.save()
|
||||
|
||||
def test_adjust_incoming_rate(self):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1
|
||||
)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 100)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
# Reduce the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 100)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 50
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 50)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0
|
||||
)
|
||||
|
||||
# Don't adjust incoming rate
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 100)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 50
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 100)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_item_less_defaults(self):
|
||||
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
|
@ -38,8 +38,11 @@
|
||||
{% if(data[i].posting_date) { %}
|
||||
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
|
||||
<td>{%= data[i].voucher_type %}
|
||||
<br>{%= data[i].voucher_no %}</td>
|
||||
<td>
|
||||
<br>{%= data[i].voucher_no %}
|
||||
</td>
|
||||
{% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
|
||||
<td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
|
||||
<span>
|
||||
{% if(!(filters.party || filters.account)) { %}
|
||||
{%= data[i].party || data[i].account %}
|
||||
<br>
|
||||
@ -49,11 +52,14 @@
|
||||
{% if(data[i].bill_no) { %}
|
||||
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
{% } %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td>
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}
|
||||
</td>
|
||||
{% } else { %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
@ -18,6 +18,7 @@
|
||||
"pr_required",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"disable_last_purchase_rate",
|
||||
@ -147,6 +148,14 @@
|
||||
"fieldname": "show_pay_button",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Pay Button in Purchase Order Portal"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.maintain_same_rate",
|
||||
"description": "Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.",
|
||||
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Landed Cost Based on Purchase Invoice Rate"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@ -154,7 +163,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-15 14:42:10.200679",
|
||||
"modified": "2023-02-28 15:41:32.686805",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
@ -21,3 +21,10 @@ class BuyingSettings(Document):
|
||||
self.get("supp_master_name") == "Naming Series",
|
||||
hide_name_field=False,
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.check_maintain_same_rate()
|
||||
|
||||
def check_maintain_same_rate(self):
|
||||
if self.maintain_same_rate:
|
||||
self.set_landed_cost_based_on_purchase_invoice_rate = 0
|
||||
|
@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Item To Be Received"] = {
|
||||
fieldname:"from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1
|
||||
},
|
||||
]
|
||||
|
@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
|
||||
fieldname:"from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1
|
||||
},
|
||||
]
|
||||
|
@ -265,7 +265,10 @@ class BuyingController(SubcontractingController):
|
||||
) / qty_in_stock_uom
|
||||
else:
|
||||
item.valuation_rate = (
|
||||
item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
item.base_net_amount
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("rate_difference_with_purchase_invoice"))
|
||||
) / qty_in_stock_uom
|
||||
else:
|
||||
item.valuation_rate = 0.0
|
||||
|
@ -24,11 +24,19 @@ class calculate_taxes_and_totals(object):
|
||||
def __init__(self, doc: Document):
|
||||
self.doc = doc
|
||||
frappe.flags.round_off_applicable_accounts = []
|
||||
|
||||
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||
|
||||
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
|
||||
self.calculate()
|
||||
|
||||
def filter_rows(self):
|
||||
"""Exclude rows, that do not fulfill the filter criteria, from totals computation."""
|
||||
items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
|
||||
return items
|
||||
|
||||
def calculate(self):
|
||||
if not len(self.doc.get("items")):
|
||||
if not len(self._items):
|
||||
return
|
||||
|
||||
self.discount_amount_applied = False
|
||||
@ -70,7 +78,7 @@ class calculate_taxes_and_totals(object):
|
||||
if hasattr(self.doc, "tax_withholding_net_total"):
|
||||
sum_net_amount = 0
|
||||
sum_base_net_amount = 0
|
||||
for item in self.doc.get("items"):
|
||||
for item in self._items:
|
||||
if hasattr(item, "apply_tds") and item.apply_tds:
|
||||
sum_net_amount += item.net_amount
|
||||
sum_base_net_amount += item.base_net_amount
|
||||
@ -79,7 +87,7 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.base_tax_withholding_net_total = sum_base_net_amount
|
||||
|
||||
def validate_item_tax_template(self):
|
||||
for item in self.doc.get("items"):
|
||||
for item in self._items:
|
||||
if item.item_code and item.get("item_tax_template"):
|
||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||
args = {
|
||||
@ -137,7 +145,7 @@ class calculate_taxes_and_totals(object):
|
||||
return
|
||||
|
||||
if not self.discount_amount_applied:
|
||||
for item in self.doc.get("items"):
|
||||
for item in self._items:
|
||||
self.doc.round_floats_in(item)
|
||||
|
||||
if item.discount_percentage == 100:
|
||||
@ -236,7 +244,7 @@ class calculate_taxes_and_totals(object):
|
||||
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
||||
return
|
||||
|
||||
for item in self.doc.get("items"):
|
||||
for item in self._items:
|
||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||
cumulated_tax_fraction = 0
|
||||
total_inclusive_tax_amount_per_qty = 0
|
||||
@ -317,7 +325,7 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.total
|
||||
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
|
||||
|
||||
for item in self.doc.get("items"):
|
||||
for item in self._items:
|
||||
self.doc.total += item.amount
|
||||
self.doc.total_qty += item.qty
|
||||
self.doc.base_total += item.base_amount
|
||||
@ -354,7 +362,7 @@ class calculate_taxes_and_totals(object):
|
||||
]
|
||||
)
|
||||
|
||||
for n, item in enumerate(self.doc.get("items")):
|
||||
for n, item in enumerate(self._items):
|
||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||
for i, tax in enumerate(self.doc.get("taxes")):
|
||||
# tax_amount represents the amount of tax for the current step
|
||||
@ -363,7 +371,7 @@ class calculate_taxes_and_totals(object):
|
||||
# Adjust divisional loss to the last item
|
||||
if tax.charge_type == "Actual":
|
||||
actual_tax_dict[tax.idx] -= current_tax_amount
|
||||
if n == len(self.doc.get("items")) - 1:
|
||||
if n == len(self._items) - 1:
|
||||
current_tax_amount += actual_tax_dict[tax.idx]
|
||||
|
||||
# accumulate tax amount into tax.tax_amount
|
||||
@ -391,7 +399,7 @@ class calculate_taxes_and_totals(object):
|
||||
)
|
||||
|
||||
# set precision in the last item iteration
|
||||
if n == len(self.doc.get("items")) - 1:
|
||||
if n == len(self._items) - 1:
|
||||
self.round_off_totals(tax)
|
||||
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
|
||||
|
||||
@ -570,7 +578,7 @@ class calculate_taxes_and_totals(object):
|
||||
def calculate_total_net_weight(self):
|
||||
if self.doc.meta.get_field("total_net_weight"):
|
||||
self.doc.total_net_weight = 0.0
|
||||
for d in self.doc.items:
|
||||
for d in self._items:
|
||||
if d.total_weight:
|
||||
self.doc.total_net_weight += d.total_weight
|
||||
|
||||
@ -630,7 +638,7 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
if total_for_discount_amount:
|
||||
# calculate item amount after Discount Amount
|
||||
for i, item in enumerate(self.doc.get("items")):
|
||||
for i, item in enumerate(self._items):
|
||||
distributed_amount = (
|
||||
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
|
||||
)
|
||||
@ -643,7 +651,7 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.apply_discount_on == "Net Total"
|
||||
or not taxes
|
||||
or total_for_discount_amount == self.doc.net_total
|
||||
) and i == len(self.doc.get("items")) - 1:
|
||||
) and i == len(self._items) - 1:
|
||||
discount_amount_loss = flt(
|
||||
self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total")
|
||||
)
|
||||
|
@ -91,6 +91,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
_calculate_taxes_and_totals() {
|
||||
const is_quotation = this.frm.doc.doctype == "Quotation";
|
||||
this.frm.doc._items = is_quotation ? this.filtered_items() : this.frm.doc.items;
|
||||
|
||||
this.validate_conversion_rate();
|
||||
this.calculate_item_values();
|
||||
this.initialize_taxes();
|
||||
@ -122,7 +125,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
calculate_item_values() {
|
||||
var me = this;
|
||||
if (!this.discount_amount_applied) {
|
||||
for (const item of this.frm.doc.items || []) {
|
||||
for (const item of this.frm.doc._items || []) {
|
||||
frappe.model.round_floats_in(item);
|
||||
item.net_rate = item.rate;
|
||||
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
|
||||
@ -131,8 +134,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||
}
|
||||
else {
|
||||
let qty = item.qty || 1;
|
||||
qty = me.frm.doc.is_return ? -1 * qty : qty;
|
||||
// allow for '0' qty on Credit/Debit notes
|
||||
let qty = item.qty || -1
|
||||
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
|
||||
}
|
||||
|
||||
@ -206,7 +209,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
});
|
||||
if(has_inclusive_tax==false) return;
|
||||
|
||||
$.each(me.frm.doc["items"] || [], function(n, item) {
|
||||
$.each(me.frm.doc._items || [], function(n, item) {
|
||||
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
||||
var cumulated_tax_fraction = 0.0;
|
||||
var total_inclusive_tax_amount_per_qty = 0;
|
||||
@ -277,7 +280,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var me = this;
|
||||
this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0;
|
||||
|
||||
$.each(this.frm.doc["items"] || [], function(i, item) {
|
||||
$.each(this.frm.doc._items || [], function(i, item) {
|
||||
me.frm.doc.total += item.amount;
|
||||
me.frm.doc.total_qty += item.qty;
|
||||
me.frm.doc.base_total += item.base_amount;
|
||||
@ -330,7 +333,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
});
|
||||
|
||||
$.each(this.frm.doc["items"] || [], function(n, item) {
|
||||
$.each(this.frm.doc._items || [], function(n, item) {
|
||||
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
||||
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
|
||||
// tax_amount represents the amount of tax for the current step
|
||||
@ -339,7 +342,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
// Adjust divisional loss to the last item
|
||||
if (tax.charge_type == "Actual") {
|
||||
actual_tax_dict[tax.idx] -= current_tax_amount;
|
||||
if (n == me.frm.doc["items"].length - 1) {
|
||||
if (n == me.frm.doc._items.length - 1) {
|
||||
current_tax_amount += actual_tax_dict[tax.idx];
|
||||
}
|
||||
}
|
||||
@ -376,7 +379,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
// set precision in the last item iteration
|
||||
if (n == me.frm.doc["items"].length - 1) {
|
||||
if (n == me.frm.doc._items.length - 1) {
|
||||
me.round_off_totals(tax);
|
||||
me.set_in_company_currency(tax,
|
||||
["tax_amount", "tax_amount_after_discount_amount"]);
|
||||
@ -599,10 +602,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
_cleanup() {
|
||||
this.frm.doc.base_in_words = this.frm.doc.in_words = "";
|
||||
let items = this.frm.doc._items;
|
||||
|
||||
if(this.frm.doc["items"] && this.frm.doc["items"].length) {
|
||||
if(!frappe.meta.get_docfield(this.frm.doc["items"][0].doctype, "item_tax_amount", this.frm.doctype)) {
|
||||
$.each(this.frm.doc["items"] || [], function(i, item) {
|
||||
if(items && items.length) {
|
||||
if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
|
||||
$.each(items || [], function(i, item) {
|
||||
delete item["item_tax_amount"];
|
||||
});
|
||||
}
|
||||
@ -655,7 +659,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var net_total = 0;
|
||||
// calculate item amount after Discount Amount
|
||||
if (total_for_discount_amount) {
|
||||
$.each(this.frm.doc["items"] || [], function(i, item) {
|
||||
$.each(this.frm.doc._items || [], function(i, item) {
|
||||
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
||||
item.net_amount = flt(item.net_amount - distributed_amount,
|
||||
precision("base_amount", item));
|
||||
@ -663,7 +667,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
// discount amount rounding loss adjustment if no taxes
|
||||
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
|
||||
&& i == (me.frm.doc.items || []).length - 1) {
|
||||
&& i == (me.frm.doc._items || []).length - 1) {
|
||||
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
|
||||
- me.frm.doc.discount_amount, precision("net_total"));
|
||||
item.net_amount = flt(item.net_amount + discount_amount_loss,
|
||||
@ -892,4 +896,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
filtered_items() {
|
||||
return this.frm.doc.items.filter(item => !item["is_alternative"]);
|
||||
}
|
||||
};
|
||||
|
@ -488,7 +488,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
() => {
|
||||
var d = locals[cdt][cdn];
|
||||
me.add_taxes_from_item_tax_template(d.item_tax_rate);
|
||||
if (d.free_item_data) {
|
||||
if (d.free_item_data && d.free_item_data.length > 0) {
|
||||
me.apply_product_discount(d);
|
||||
}
|
||||
},
|
||||
|
@ -90,7 +90,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
|| frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
|
||||
this.frm.add_custom_button(
|
||||
__("Sales Order"),
|
||||
this.frm.cscript["Make Sales Order"],
|
||||
() => this.make_sales_order(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
@ -145,6 +145,20 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
|
||||
}
|
||||
|
||||
make_sales_order() {
|
||||
var me = this;
|
||||
|
||||
let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative);
|
||||
if (has_alternative_item) {
|
||||
this.show_alternative_items_dialog();
|
||||
} else {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
||||
frm: me.frm
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
set_dynamic_field_label(){
|
||||
if (this.frm.doc.quotation_to == "Customer")
|
||||
{
|
||||
@ -220,17 +234,111 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
show_alternative_items_dialog() {
|
||||
let me = this;
|
||||
|
||||
const table_fields = [
|
||||
{
|
||||
fieldtype:"Data",
|
||||
fieldname:"name",
|
||||
label: __("Name"),
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldtype:"Link",
|
||||
fieldname:"item_code",
|
||||
options: "Item",
|
||||
label: __("Item Code"),
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
columns: 2,
|
||||
formatter: (value, df, options, doc) => {
|
||||
return doc.is_alternative ? `<span class="indicator yellow">${value}</span>` : value;
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype:"Data",
|
||||
fieldname:"description",
|
||||
label: __("Description"),
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldtype:"Currency",
|
||||
fieldname:"amount",
|
||||
label: __("Amount"),
|
||||
options: "currency",
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldtype:"Check",
|
||||
fieldname:"is_alternative",
|
||||
label: __("Is Alternative"),
|
||||
read_only: 1,
|
||||
}];
|
||||
|
||||
|
||||
this.data = this.frm.doc.items.filter(
|
||||
(item) => item.is_alternative || item.has_alternative_item
|
||||
).map((item) => {
|
||||
return {
|
||||
"name": item.name,
|
||||
"item_code": item.item_code,
|
||||
"description": item.description,
|
||||
"amount": item.amount,
|
||||
"is_alternative": item.is_alternative,
|
||||
}
|
||||
});
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Alternative Items for Sales Order"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "info",
|
||||
fieldtype: "HTML",
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
fieldname: "alternative_items",
|
||||
fieldtype: "Table",
|
||||
cannot_add_rows: true,
|
||||
in_place_edit: true,
|
||||
reqd: 1,
|
||||
data: this.data,
|
||||
description: __("Select an item from each set to be used in the Sales Order."),
|
||||
get_data: () => {
|
||||
return this.data;
|
||||
},
|
||||
fields: table_fields
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
||||
frm: me.frm,
|
||||
args: {
|
||||
selected_items: dialog.fields_dict.alternative_items.grid.get_selected_children()
|
||||
}
|
||||
});
|
||||
dialog.hide();
|
||||
},
|
||||
primary_action_label: __('Continue')
|
||||
});
|
||||
|
||||
dialog.fields_dict.info.$wrapper.html(
|
||||
`<p class="small text-muted">
|
||||
<span class="indicator yellow"></span>
|
||||
Alternative Items
|
||||
</p>`
|
||||
)
|
||||
dialog.show();
|
||||
}
|
||||
};
|
||||
|
||||
cur_frm.script_manager.make(erpnext.selling.QuotationController);
|
||||
|
||||
cur_frm.cscript['Make Sales Order'] = function() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
||||
frm: cur_frm
|
||||
})
|
||||
}
|
||||
|
||||
frappe.ui.form.on("Quotation Item", "items_on_form_rendered", "packed_items_on_form_rendered", function(frm, cdt, cdn) {
|
||||
// enable tax_amount field if Actual
|
||||
})
|
||||
|
@ -35,6 +35,9 @@ class Quotation(SellingController):
|
||||
|
||||
make_packing_list(self)
|
||||
|
||||
def before_submit(self):
|
||||
self.set_has_alternative_item()
|
||||
|
||||
def validate_valid_till(self):
|
||||
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
|
||||
frappe.throw(_("Valid till date cannot be before transaction date"))
|
||||
@ -59,7 +62,18 @@ class Quotation(SellingController):
|
||||
title=_("Unpublished Item"),
|
||||
)
|
||||
|
||||
def set_has_alternative_item(self):
|
||||
"""Mark 'Has Alternative Item' for rows."""
|
||||
if not any(row.is_alternative for row in self.get("items")):
|
||||
return
|
||||
|
||||
items_with_alternatives = self.get_rows_with_alternatives()
|
||||
for row in self.get("items"):
|
||||
if not row.is_alternative and row.name in items_with_alternatives:
|
||||
row.has_alternative_item = 1
|
||||
|
||||
def get_ordered_status(self):
|
||||
status = "Open"
|
||||
ordered_items = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Sales Order Item",
|
||||
@ -70,16 +84,40 @@ class Quotation(SellingController):
|
||||
)
|
||||
)
|
||||
|
||||
status = "Open"
|
||||
if ordered_items:
|
||||
if not ordered_items:
|
||||
return status
|
||||
|
||||
has_alternatives = any(row.is_alternative for row in self.get("items"))
|
||||
self._items = self.get_valid_items() if has_alternatives else self.get("items")
|
||||
|
||||
if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items):
|
||||
status = "Partially Ordered"
|
||||
else:
|
||||
status = "Ordered"
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.qty > ordered_items.get(item.item_code, 0.0):
|
||||
status = "Partially Ordered"
|
||||
|
||||
return status
|
||||
|
||||
def get_valid_items(self):
|
||||
"""
|
||||
Filters out items in an alternatives set that were not ordered.
|
||||
"""
|
||||
|
||||
def is_in_sales_order(row):
|
||||
in_sales_order = bool(
|
||||
frappe.db.exists(
|
||||
"Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code, "docstatus": 1}
|
||||
)
|
||||
)
|
||||
return in_sales_order
|
||||
|
||||
def can_map(row) -> bool:
|
||||
if row.is_alternative or row.has_alternative_item:
|
||||
return is_in_sales_order(row)
|
||||
|
||||
return True
|
||||
|
||||
return list(filter(can_map, self.get("items")))
|
||||
|
||||
def is_fully_ordered(self):
|
||||
return self.get_ordered_status() == "Ordered"
|
||||
|
||||
@ -176,6 +214,22 @@ class Quotation(SellingController):
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.valid_till = None
|
||||
|
||||
def get_rows_with_alternatives(self):
|
||||
rows_with_alternatives = []
|
||||
table_length = len(self.get("items"))
|
||||
|
||||
for idx, row in enumerate(self.get("items")):
|
||||
if row.is_alternative:
|
||||
continue
|
||||
|
||||
if idx == (table_length - 1):
|
||||
break
|
||||
|
||||
if self.get("items")[idx + 1].is_alternative:
|
||||
rows_with_alternatives.append(row.name)
|
||||
|
||||
return rows_with_alternatives
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||
@ -221,6 +275,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
)
|
||||
)
|
||||
|
||||
selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])]
|
||||
|
||||
def set_missing_values(source, target):
|
||||
if customer:
|
||||
target.customer = customer.name
|
||||
@ -244,6 +300,24 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.blanket_order = obj.blanket_order
|
||||
target.blanket_order_rate = obj.blanket_order_rate
|
||||
|
||||
def can_map_row(item) -> bool:
|
||||
"""
|
||||
Row mapping from Quotation to Sales order:
|
||||
1. If no selections, map all non-alternative rows (that sum up to the grand total)
|
||||
2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty
|
||||
3. If selections: Simple row: Map if adequate qty
|
||||
"""
|
||||
has_qty = item.qty > 0
|
||||
|
||||
if not selected_rows:
|
||||
return not item.is_alternative
|
||||
|
||||
if selected_rows and (item.is_alternative or item.has_alternative_item):
|
||||
return (item.name in selected_rows) and has_qty
|
||||
|
||||
# Simple row
|
||||
return has_qty
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Quotation",
|
||||
source_name,
|
||||
@ -253,7 +327,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
"doctype": "Sales Order Item",
|
||||
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.qty > 0,
|
||||
"condition": can_map_row,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||
@ -322,7 +396,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
source_name,
|
||||
{
|
||||
"Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}},
|
||||
"Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item},
|
||||
"Quotation Item": {
|
||||
"doctype": "Sales Invoice Item",
|
||||
"postprocess": update_item,
|
||||
"condition": lambda row: not row.is_alternative,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||
},
|
||||
|
@ -457,6 +457,139 @@ class TestQuotation(FrappeTestCase):
|
||||
expected_index = id + 1
|
||||
self.assertEqual(item.idx, expected_index)
|
||||
|
||||
def test_alternative_items_with_stock_items(self):
|
||||
"""
|
||||
Check if taxes & totals considers only non-alternative items with:
|
||||
- One set of non-alternative & alternative items [first 3 rows]
|
||||
- One simple stock item
|
||||
"""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_list = []
|
||||
stock_items = {
|
||||
"_Test Simple Item 1": 100,
|
||||
"_Test Alt 1": 120,
|
||||
"_Test Alt 2": 110,
|
||||
"_Test Simple Item 2": 200,
|
||||
}
|
||||
|
||||
for item, rate in stock_items.items():
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
item_list.append(
|
||||
{
|
||||
"item_code": item,
|
||||
"qty": 1,
|
||||
"rate": rate,
|
||||
"is_alternative": bool("Alt" in item),
|
||||
}
|
||||
)
|
||||
|
||||
quotation = make_quotation(item_list=item_list, do_not_submit=1)
|
||||
quotation.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
quotation.submit()
|
||||
|
||||
self.assertEqual(quotation.net_total, 300)
|
||||
self.assertEqual(quotation.grand_total, 330)
|
||||
|
||||
def test_alternative_items_with_service_items(self):
|
||||
"""
|
||||
Check if taxes & totals considers only non-alternative items with:
|
||||
- One set of non-alternative & alternative service items [first 3 rows]
|
||||
- One simple non-alternative service item
|
||||
All having the same item code and unique item name/description due to
|
||||
dynamic services
|
||||
"""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_list = []
|
||||
service_items = {
|
||||
"Tiling with Standard Tiles": 100,
|
||||
"Alt Tiling with Durable Tiles": 150,
|
||||
"Alt Tiling with Premium Tiles": 180,
|
||||
"False Ceiling with Material #234": 190,
|
||||
}
|
||||
|
||||
make_item("_Test Dynamic Service Item", {"is_stock_item": 0})
|
||||
|
||||
for name, rate in service_items.items():
|
||||
item_list.append(
|
||||
{
|
||||
"item_code": "_Test Dynamic Service Item",
|
||||
"item_name": name,
|
||||
"description": name,
|
||||
"qty": 1,
|
||||
"rate": rate,
|
||||
"is_alternative": bool("Alt" in name),
|
||||
}
|
||||
)
|
||||
|
||||
quotation = make_quotation(item_list=item_list, do_not_submit=1)
|
||||
quotation.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
quotation.submit()
|
||||
|
||||
self.assertEqual(quotation.net_total, 290)
|
||||
self.assertEqual(quotation.grand_total, 319)
|
||||
|
||||
def test_alternative_items_sales_order_mapping_with_stock_items(self):
|
||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
frappe.flags.args = frappe._dict()
|
||||
item_list = []
|
||||
stock_items = {
|
||||
"_Test Simple Item 1": 100,
|
||||
"_Test Alt 1": 120,
|
||||
"_Test Alt 2": 110,
|
||||
"_Test Simple Item 2": 200,
|
||||
}
|
||||
|
||||
for item, rate in stock_items.items():
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
item_list.append(
|
||||
{
|
||||
"item_code": item,
|
||||
"qty": 1,
|
||||
"rate": rate,
|
||||
"is_alternative": bool("Alt" in item),
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
)
|
||||
|
||||
quotation = make_quotation(item_list=item_list)
|
||||
|
||||
frappe.flags.args.selected_items = [quotation.items[2]]
|
||||
sales_order = make_sales_order(quotation.name)
|
||||
sales_order.delivery_date = add_days(sales_order.transaction_date, 10)
|
||||
sales_order.save()
|
||||
|
||||
self.assertEqual(sales_order.items[0].item_code, "_Test Alt 2")
|
||||
self.assertEqual(sales_order.items[1].item_code, "_Test Simple Item 2")
|
||||
self.assertEqual(sales_order.net_total, 310)
|
||||
|
||||
sales_order.submit()
|
||||
quotation.reload()
|
||||
self.assertEqual(quotation.status, "Ordered")
|
||||
|
||||
|
||||
test_records = frappe.get_test_records("Quotation")
|
||||
|
||||
|
@ -49,6 +49,8 @@
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"is_alternative",
|
||||
"has_alternative_item",
|
||||
"section_break_43",
|
||||
"valuation_rate",
|
||||
"column_break_45",
|
||||
@ -643,12 +645,28 @@
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_alternative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Alternative",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_alternative_item",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Has Alternative Item",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-25 02:49:53.926625",
|
||||
"modified": "2023-02-06 11:00:07.042364",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Item",
|
||||
@ -656,5 +674,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -275,7 +275,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
if (this.frm.doc.docstatus===0) {
|
||||
this.frm.add_custom_button(__('Quotation'),
|
||||
function() {
|
||||
erpnext.utils.map_current_doc({
|
||||
let d = erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
||||
source_doctype: "Quotation",
|
||||
target: me.frm,
|
||||
@ -293,7 +293,16 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
docstatus: 1,
|
||||
status: ["!=", "Lost"]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
d.$parent.append(`
|
||||
<span class='small text-muted'>
|
||||
${__("Note: Please create Sales Orders from individual Quotations to select from among Alternative Items.")}
|
||||
</span>
|
||||
`);
|
||||
}, 200);
|
||||
|
||||
}, __("Get Items From"));
|
||||
}
|
||||
|
||||
|
@ -293,6 +293,7 @@ class PurchaseReceipt(BuyingController):
|
||||
get_purchase_document_details,
|
||||
)
|
||||
|
||||
stock_rbnb = None
|
||||
if erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
||||
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
||||
@ -450,6 +451,21 @@ class PurchaseReceipt(BuyingController):
|
||||
item=d,
|
||||
)
|
||||
|
||||
if d.rate_difference_with_purchase_invoice and stock_rbnb:
|
||||
account_currency = get_account_currency(stock_rbnb)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=stock_rbnb,
|
||||
cost_center=d.cost_center,
|
||||
debit=0.0,
|
||||
credit=flt(d.rate_difference_with_purchase_invoice),
|
||||
remarks=_("Adjustment based on Purchase Invoice rate"),
|
||||
against_account=warehouse_account_name,
|
||||
account_currency=account_currency,
|
||||
project=d.project,
|
||||
item=d,
|
||||
)
|
||||
|
||||
# sub-contracting warehouse
|
||||
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
|
||||
self.add_gl_entry(
|
||||
@ -470,6 +486,7 @@ class PurchaseReceipt(BuyingController):
|
||||
+ flt(d.landed_cost_voucher_amount)
|
||||
+ flt(d.rm_supp_cost)
|
||||
+ flt(d.item_tax_amount)
|
||||
+ flt(d.rate_difference_with_purchase_invoice)
|
||||
)
|
||||
|
||||
divisional_loss = flt(
|
||||
@ -765,7 +782,7 @@ class PurchaseReceipt(BuyingController):
|
||||
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
|
||||
|
||||
for pr in set(updated_pr):
|
||||
pr_doc = self if (pr == self.name) else frappe.get_cached_doc("Purchase Receipt", pr)
|
||||
pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr)
|
||||
update_billing_percentage(pr_doc, update_modified=update_modified)
|
||||
|
||||
self.load_from_db()
|
||||
@ -881,7 +898,7 @@ def get_billed_amount_against_po(po_items):
|
||||
return {d.po_detail: flt(d.billed_amt) for d in query}
|
||||
|
||||
|
||||
def update_billing_percentage(pr_doc, update_modified=True):
|
||||
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
|
||||
# Reload as billed amount was set in db directly
|
||||
pr_doc.load_from_db()
|
||||
|
||||
@ -897,6 +914,12 @@ def update_billing_percentage(pr_doc, update_modified=True):
|
||||
|
||||
total_amount += total_billable_amount
|
||||
total_billed_amount += flt(item.billed_amt)
|
||||
if adjust_incoming_rate:
|
||||
adjusted_amt = 0.0
|
||||
if item.billed_amt and item.amount:
|
||||
adjusted_amt = flt(item.billed_amt) - flt(item.amount)
|
||||
|
||||
item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
||||
|
||||
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
|
||||
pr_doc.db_set("per_billed", percent_billed)
|
||||
@ -906,6 +929,26 @@ def update_billing_percentage(pr_doc, update_modified=True):
|
||||
pr_doc.set_status(update=True)
|
||||
pr_doc.notify_update()
|
||||
|
||||
if adjust_incoming_rate:
|
||||
adjust_incoming_rate_for_pr(pr_doc)
|
||||
|
||||
|
||||
def adjust_incoming_rate_for_pr(doc):
|
||||
doc.update_valuation_rate(reset_outgoing_rate=False)
|
||||
|
||||
for item in doc.get("items"):
|
||||
item.db_update()
|
||||
|
||||
doc.docstatus = 2
|
||||
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
# update stock & gl entries for submit state of PR
|
||||
doc.docstatus = 1
|
||||
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||
doc.make_gl_entries()
|
||||
doc.repost_future_sle_and_gle()
|
||||
|
||||
|
||||
def get_item_wise_returned_qty(pr_doc):
|
||||
items = [d.name for d in pr_doc.items]
|
||||
|
@ -69,6 +69,7 @@
|
||||
"item_tax_amount",
|
||||
"rm_supp_cost",
|
||||
"landed_cost_voucher_amount",
|
||||
"rate_difference_with_purchase_invoice",
|
||||
"billed_amt",
|
||||
"warehouse_and_reference",
|
||||
"warehouse",
|
||||
@ -1007,12 +1008,20 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Item Scanned",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rate_difference_with_purchase_invoice",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate Difference with Purchase Invoice",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-18 15:48:58.114923",
|
||||
"modified": "2023-02-28 15:43:04.470104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, cstr, flt
|
||||
from frappe.utils import cint, cstr, flt, get_number_format_info
|
||||
|
||||
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import (
|
||||
get_template_details,
|
||||
@ -156,7 +156,9 @@ class QualityInspection(Document):
|
||||
for i in range(1, 11):
|
||||
reading_value = reading.get("reading_" + str(i))
|
||||
if reading_value is not None and reading_value.strip():
|
||||
result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
|
||||
result = (
|
||||
flt(reading.get("min_value")) <= parse_float(reading_value) <= flt(reading.get("max_value"))
|
||||
)
|
||||
if not result:
|
||||
return False
|
||||
return True
|
||||
@ -196,7 +198,7 @@ class QualityInspection(Document):
|
||||
# numeric readings
|
||||
for i in range(1, 11):
|
||||
field = "reading_" + str(i)
|
||||
data[field] = flt(reading.get(field))
|
||||
data[field] = parse_float(reading.get(field))
|
||||
data["mean"] = self.calculate_mean(reading)
|
||||
|
||||
return data
|
||||
@ -210,7 +212,7 @@ class QualityInspection(Document):
|
||||
for i in range(1, 11):
|
||||
reading_value = reading.get("reading_" + str(i))
|
||||
if reading_value is not None and reading_value.strip():
|
||||
readings_list.append(flt(reading_value))
|
||||
readings_list.append(parse_float(reading_value))
|
||||
|
||||
actual_mean = mean(readings_list) if readings_list else 0
|
||||
return actual_mean
|
||||
@ -324,3 +326,19 @@ def make_quality_inspection(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def parse_float(num: str) -> float:
|
||||
"""Since reading_# fields are `Data` field they might contain number which
|
||||
is representation in user's prefered number format instead of machine
|
||||
readable format. This function converts them to machine readable format."""
|
||||
|
||||
number_format = frappe.db.get_default("number_format") or "#,###.##"
|
||||
decimal_str, comma_str, _number_format_precision = get_number_format_info(number_format)
|
||||
|
||||
if decimal_str == "," and comma_str == ".":
|
||||
num = num.replace(",", "#$")
|
||||
num = num.replace(".", ",")
|
||||
num = num.replace("#$", ".")
|
||||
|
||||
return flt(num)
|
||||
|
@ -2,7 +2,7 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.controllers.stock_controller import (
|
||||
@ -216,6 +216,40 @@ class TestQualityInspection(FrappeTestCase):
|
||||
qa.save()
|
||||
self.assertEqual(qa.status, "Accepted")
|
||||
|
||||
@change_settings("System Settings", {"number_format": "#.###,##"})
|
||||
def test_diff_number_format(self):
|
||||
self.assertEqual(frappe.db.get_default("number_format"), "#.###,##") # sanity check
|
||||
|
||||
# Test QI based on acceptance values (Non formula)
|
||||
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||
readings = [
|
||||
{
|
||||
"specification": "Iron Content", # numeric reading
|
||||
"min_value": 60,
|
||||
"max_value": 100,
|
||||
"reading_1": "70,000",
|
||||
},
|
||||
{
|
||||
"specification": "Iron Content", # numeric reading
|
||||
"min_value": 60,
|
||||
"max_value": 100,
|
||||
"reading_1": "1.100,00",
|
||||
},
|
||||
]
|
||||
|
||||
qa = create_quality_inspection(
|
||||
reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True
|
||||
)
|
||||
|
||||
qa.save()
|
||||
|
||||
# status must be auto set as per formula
|
||||
self.assertEqual(qa.readings[0].status, "Accepted")
|
||||
self.assertEqual(qa.readings[1].status, "Rejected")
|
||||
|
||||
qa.delete()
|
||||
dn.delete()
|
||||
|
||||
|
||||
def create_quality_inspection(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -191,14 +191,17 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
|
||||
def validate_available_qty_for_consumption(self):
|
||||
for item in self.get("supplied_items"):
|
||||
precision = item.precision("consumed_qty")
|
||||
if (
|
||||
item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty
|
||||
item.available_qty_for_consumption
|
||||
and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table."
|
||||
).format(item.idx)
|
||||
)
|
||||
msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)}
|
||||
must be less than or equal to Available Qty For Consumption
|
||||
{flt(item.available_qty_for_consumption, precision)}
|
||||
in Consumed Items Table."""
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def validate_items_qty(self):
|
||||
for item in self.items:
|
||||
|
Loading…
x
Reference in New Issue
Block a user