mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-23 08:38:30 +00:00
Merge pull request #43467 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -109,7 +109,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "frankfurter.app/{transaction_date}"
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -210,19 +210,31 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dunning_letter_text(dunning_type, doc, language=None):
|
||||
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
|
||||
DOCTYPE = "Dunning Letter Text"
|
||||
FIELDS = ["body_text", "closing_text", "language"]
|
||||
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if not language:
|
||||
language = doc.get("language")
|
||||
|
||||
if language:
|
||||
filters = {"parent": dunning_type, "language": language}
|
||||
else:
|
||||
filters = {"parent": dunning_type, "is_default_language": 1}
|
||||
letter_text = frappe.db.get_value(
|
||||
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
|
||||
)
|
||||
if letter_text:
|
||||
return {
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
|
||||
)
|
||||
|
||||
if not letter_text:
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
|
||||
)
|
||||
|
||||
if not letter_text:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
|
||||
@@ -2292,6 +2292,24 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_last_purchase_rate(self):
|
||||
item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1)
|
||||
pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100)
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 100)
|
||||
|
||||
pi2 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=200)
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 200)
|
||||
|
||||
pi2.cancel()
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 100)
|
||||
|
||||
pi1.cancel()
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 0)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -3309,7 +3309,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||
any_conversion_factor_changed = False
|
||||
|
||||
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_doc_permissions(parent, "write")
|
||||
@@ -3425,25 +3424,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
# if rate is greater than price_list_rate, set margin
|
||||
# or set discount
|
||||
child_item.discount_percentage = 0
|
||||
|
||||
if parent_doctype in sales_doctypes:
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.discount_percentage = flt(
|
||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||
child_item.precision("discount_percentage"),
|
||||
)
|
||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||
|
||||
if parent_doctype in sales_doctypes:
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
|
||||
child_item.flags.ignore_validate_update_after_submit = True
|
||||
if new_child_flag:
|
||||
|
||||
@@ -702,9 +702,11 @@ class BuyingController(SubcontractingController):
|
||||
if self.get("is_return"):
|
||||
return
|
||||
|
||||
if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
|
||||
"Buying Settings", "disable_last_purchase_rate"
|
||||
):
|
||||
if self.doctype in [
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
|
||||
update_last_purchase_rate(self, is_submit=0)
|
||||
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
|
||||
@@ -64,6 +64,18 @@ class StockController(AccountsController):
|
||||
self.validate_internal_transfer()
|
||||
self.validate_putaway_capacity()
|
||||
|
||||
def validate_items_exist(self):
|
||||
if not self.get("items"):
|
||||
return
|
||||
|
||||
items = [d.item_code for d in self.get("items")]
|
||||
|
||||
exists_items = frappe.get_all("Item", filters={"name": ("in", items)}, pluck="name")
|
||||
non_exists_items = set(items) - set(exists_items)
|
||||
|
||||
if non_exists_items:
|
||||
frappe.throw(_("Items {0} do not exist in the Item master.").format(", ".join(non_exists_items)))
|
||||
|
||||
def validate_duplicate_serial_and_batch_bundle(self, table_name):
|
||||
if not self.get(table_name):
|
||||
return
|
||||
|
||||
@@ -40,7 +40,7 @@ class calculate_taxes_and_totals:
|
||||
return items
|
||||
|
||||
def calculate(self):
|
||||
if not len(self._items):
|
||||
if not len(self.doc.items):
|
||||
return
|
||||
|
||||
self.discount_amount_applied = False
|
||||
@@ -95,7 +95,7 @@ class calculate_taxes_and_totals:
|
||||
if self.doc.get("is_return") and self.doc.get("return_against"):
|
||||
return
|
||||
|
||||
for item in self._items:
|
||||
for item in self.doc.items:
|
||||
if item.item_code and item.get("item_tax_template"):
|
||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||
args = {
|
||||
@@ -154,7 +154,7 @@ class calculate_taxes_and_totals:
|
||||
return
|
||||
|
||||
if not self.discount_amount_applied:
|
||||
for item in self._items:
|
||||
for item in self.doc.items:
|
||||
self.doc.round_floats_in(item)
|
||||
|
||||
if item.discount_percentage == 100:
|
||||
@@ -258,7 +258,7 @@ class calculate_taxes_and_totals:
|
||||
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
||||
return
|
||||
|
||||
for item in self._items:
|
||||
for item in self.doc.items:
|
||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||
cumulated_tax_fraction = 0
|
||||
total_inclusive_tax_amount_per_qty = 0
|
||||
|
||||
@@ -1531,6 +1531,197 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
self.assertFalse(serial_nos)
|
||||
|
||||
def test_backflushed_batch_raw_materials_based_on_transferred_autosabb(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings",
|
||||
"backflush_raw_materials_based_on",
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
batch_item = "Test Batch MCC Keyboard"
|
||||
fg_item = "Test FG Item with Batch Raw Materials"
|
||||
|
||||
ste_doc = test_stock_entry.make_stock_entry(
|
||||
item_code=batch_item, target="Stores - _TC", qty=8, basic_rate=100, do_not_save=True
|
||||
)
|
||||
|
||||
# Inward raw materials in Stores warehouse
|
||||
ste_doc.submit()
|
||||
ste_doc.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle)
|
||||
|
||||
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
|
||||
# action taken upon Start button:
|
||||
transferred_ste_doc = frappe.get_doc(
|
||||
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
|
||||
)
|
||||
|
||||
transferred_ste_doc.submit()
|
||||
transferred_ste_doc.reload()
|
||||
|
||||
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
get_batch_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle), batch_no
|
||||
)
|
||||
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
|
||||
|
||||
# Make additional consumption and link to WO
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="Test Batch Battery Consumable",
|
||||
target="Stores - _TC",
|
||||
qty=8,
|
||||
basic_rate=2.33,
|
||||
)
|
||||
consume_use_doc = test_stock_entry.make_stock_entry(
|
||||
item_code="Test Batch Battery Consumable", # consumable not linked to BOM
|
||||
source="Stores - _TC",
|
||||
qty=4,
|
||||
purpose="Material Consumption for Manufacture",
|
||||
do_not_save=True,
|
||||
)
|
||||
consume_use_doc.work_order = wo_doc.name
|
||||
consume_use_doc.fg_completed_qty = 4
|
||||
consume_use_doc.submit()
|
||||
consume_use_doc.reload()
|
||||
|
||||
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
|
||||
mfr_items = [i.as_dict() for i in manufacture_ste_doc.items]
|
||||
manufacture_ste_doc.submit()
|
||||
manufacture_ste_doc.reload()
|
||||
|
||||
self.assertTrue(len(mfr_items), 2)
|
||||
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
get_batch_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle), batch_no
|
||||
)
|
||||
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
|
||||
|
||||
def test_backflushed_serial_no_raw_materials_based_on_transferred_autosabb(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings",
|
||||
"backflush_raw_materials_based_on",
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
sn_item = "Test Serial No BTT Headphone"
|
||||
fg_item = "Test FG Item with Serial No Raw Materials"
|
||||
|
||||
ste_doc = test_stock_entry.make_stock_entry(
|
||||
item_code=sn_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
|
||||
)
|
||||
|
||||
# Inward raw materials in Stores warehouse
|
||||
ste_doc.submit()
|
||||
ste_doc.reload()
|
||||
|
||||
serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle))
|
||||
|
||||
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
|
||||
transferred_ste_doc = frappe.get_doc(
|
||||
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
|
||||
)
|
||||
|
||||
transferred_ste_doc.submit()
|
||||
transferred_ste_doc.reload()
|
||||
|
||||
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
sorted(get_serial_nos_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle)),
|
||||
serial_nos_list,
|
||||
)
|
||||
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
|
||||
|
||||
# Make additional consumption and link to WO
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="Test Serial Battery Consumable",
|
||||
target="Stores - _TC",
|
||||
qty=8,
|
||||
basic_rate=3.33,
|
||||
)
|
||||
consume_use_doc = test_stock_entry.make_stock_entry(
|
||||
item_code="Test Serial Battery Consumable", # consumable not linked to BOM
|
||||
source="Stores - _TC",
|
||||
qty=4,
|
||||
purpose="Material Consumption for Manufacture",
|
||||
do_not_save=True,
|
||||
)
|
||||
consume_use_doc.work_order = wo_doc.name
|
||||
consume_use_doc.fg_completed_qty = 4
|
||||
consume_use_doc.submit()
|
||||
consume_use_doc.reload()
|
||||
|
||||
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
|
||||
mfr_items = [i.as_dict() for i in manufacture_ste_doc.items]
|
||||
manufacture_ste_doc.submit()
|
||||
manufacture_ste_doc.reload()
|
||||
|
||||
self.assertTrue(len(mfr_items), 2)
|
||||
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
sorted(get_serial_nos_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle)),
|
||||
serial_nos_list,
|
||||
)
|
||||
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
|
||||
|
||||
def test_backflushed_serial_no_batch_raw_materials_based_on_transferred_autosabb(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings",
|
||||
"backflush_raw_materials_based_on",
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
sn_batch_item = "Test Batch Serial No WebCam"
|
||||
fg_item = "Test FG Item with Serial & Batch No Raw Materials"
|
||||
|
||||
ste_doc = test_stock_entry.make_stock_entry(
|
||||
item_code=sn_batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
|
||||
)
|
||||
|
||||
ste_doc.submit()
|
||||
ste_doc.reload()
|
||||
|
||||
serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle))
|
||||
batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle)
|
||||
|
||||
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
|
||||
transferred_ste_doc = frappe.get_doc(
|
||||
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
|
||||
)
|
||||
|
||||
transferred_ste_doc.submit()
|
||||
transferred_ste_doc.reload()
|
||||
|
||||
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
sorted(get_serial_nos_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle)),
|
||||
serial_nos_list,
|
||||
)
|
||||
self.assertEqual(
|
||||
get_batch_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle), batch_no
|
||||
)
|
||||
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
|
||||
|
||||
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
|
||||
manufacture_ste_doc.submit()
|
||||
manufacture_ste_doc.reload()
|
||||
|
||||
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
sorted(get_serial_nos_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle)),
|
||||
serial_nos_list,
|
||||
)
|
||||
self.assertEqual(
|
||||
get_batch_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle), batch_no
|
||||
)
|
||||
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
|
||||
|
||||
bundle = manufacture_ste_doc.items[0].serial_and_batch_bundle
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle)
|
||||
qty = sum(e.qty for e in bundle_doc.entries)
|
||||
self.assertEqual(qty, -4.0)
|
||||
|
||||
###
|
||||
def test_non_consumed_material_return_against_work_order(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings",
|
||||
@@ -2335,6 +2526,29 @@ def prepare_data_for_backflush_based_on_materials_transferred():
|
||||
|
||||
make_bom(item=item.name, source_warehouse="Stores - _TC", raw_materials=[batch_item_doc.name])
|
||||
|
||||
# Make additional items not attached to a BOM
|
||||
make_item(
|
||||
"Test Batch Battery Consumable",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBMK.#####",
|
||||
"valuation_rate": 2.33,
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
)
|
||||
make_item(
|
||||
"Test Serial Battery Consumable",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TSBH.#####",
|
||||
"valuation_rate": 3.33,
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
)
|
||||
|
||||
sn_item_doc = make_item(
|
||||
"Test Serial No BTT Headphone",
|
||||
{
|
||||
|
||||
@@ -377,3 +377,4 @@ erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
|
||||
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
|
||||
erpnext.patches.v15_0.set_standard_stock_entry_type
|
||||
erpnext.patches.v15_0.link_purchase_item_to_asset_doc
|
||||
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
settings = frappe.get_doc("Currency Exchange Settings")
|
||||
if settings.service_provider != "frankfurter.app":
|
||||
return
|
||||
|
||||
settings.set_parameters_and_result()
|
||||
settings.flags.ignore_validate = True
|
||||
settings.save()
|
||||
@@ -342,12 +342,15 @@ erpnext.buying = {
|
||||
add_serial_batch_bundle(doc, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let me = this;
|
||||
let fields = ["has_batch_no", "has_serial_no"];
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
|
||||
frappe.db.get_value("Item", item.item_code, fields)
|
||||
.then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
fields.forEach((field) => {
|
||||
item[field] = r.message[field];
|
||||
});
|
||||
|
||||
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
|
||||
item.is_rejected = false;
|
||||
|
||||
@@ -380,12 +383,15 @@ erpnext.buying = {
|
||||
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let me = this;
|
||||
let fields = ["has_batch_no", "has_serial_no"];
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
|
||||
frappe.db.get_value("Item", item.item_code, fields)
|
||||
.then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
fields.forEach((field) => {
|
||||
item[field] = r.message[field];
|
||||
});
|
||||
|
||||
item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward";
|
||||
item.is_rejected = true;
|
||||
|
||||
|
||||
@@ -128,7 +128,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._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;
|
||||
@@ -227,7 +227,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
});
|
||||
if(has_inclusive_tax==false) return;
|
||||
|
||||
$.each(me.frm._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);
|
||||
var cumulated_tax_fraction = 0.0;
|
||||
var total_inclusive_tax_amount_per_qty = 0;
|
||||
@@ -630,7 +630,7 @@ 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._items;
|
||||
let items = this.frm.doc.items;
|
||||
|
||||
if(items && items.length) {
|
||||
if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
|
||||
|
||||
@@ -573,6 +573,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
if (item.docstatus === 0
|
||||
&& frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
|
||||
&& !item.use_serial_batch_fields
|
||||
&& cint(frappe.user_defaults?.use_serial_batch_fields) === 1
|
||||
) {
|
||||
item["use_serial_batch_fields"] = 1;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
var d = locals[cdt][cdn];
|
||||
me.add_taxes_from_item_tax_template(d.item_tax_rate);
|
||||
@@ -1102,7 +1111,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
apply_discount_on_item(doc, cdt, cdn, field) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if(!item.price_list_rate) {
|
||||
if(!item?.price_list_rate) {
|
||||
item[field] = 0.0;
|
||||
} else {
|
||||
this.price_list_rate(doc, cdt, cdn);
|
||||
@@ -1268,6 +1277,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
.filter(Boolean).length > 0;
|
||||
} else if (this.frm.doc?.items) {
|
||||
let first_row = this.frm.doc.items[0];
|
||||
if (!first_row) {
|
||||
return false
|
||||
};
|
||||
|
||||
let mapped_rows = mappped_fields.filter(d => first_row[d])
|
||||
|
||||
return mapped_rows?.length > 0;
|
||||
|
||||
@@ -16,7 +16,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
let label = this.item?.has_serial_no ? __("Serial Nos") : __("Batch Nos");
|
||||
let primary_label = this.bundle ? __("Update") : __("Add");
|
||||
|
||||
if (this.item?.has_serial_no && this.item?.batch_no) {
|
||||
if (this.item?.has_serial_no && this.item?.has_batch_no) {
|
||||
label = __("Serial Nos / Batch Nos");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: this.item?.title || primary_label,
|
||||
size: "large",
|
||||
fields: this.get_dialog_fields(),
|
||||
primary_action_label: primary_label,
|
||||
primary_action: () => this.update_bundle_entries(),
|
||||
@@ -164,12 +165,14 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
|
||||
fields.push({
|
||||
fieldtype: "Section Break",
|
||||
depends_on: "eval:doc.enter_manually !== 1 || doc.entries?.length > 0",
|
||||
});
|
||||
|
||||
fields.push({
|
||||
fieldname: "entries",
|
||||
fieldtype: "Table",
|
||||
allow_bulk_edit: true,
|
||||
depends_on: "eval:doc.enter_manually !== 1 || doc.entries?.length > 0",
|
||||
data: [],
|
||||
fields: this.get_dialog_table_fields(),
|
||||
});
|
||||
@@ -178,6 +181,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
}
|
||||
|
||||
get_attach_field() {
|
||||
let me = this;
|
||||
let label = this.item?.has_serial_no ? __("Serial Nos") : __("Batch Nos");
|
||||
let primary_label = this.bundle ? __("Update") : __("Add");
|
||||
|
||||
@@ -185,66 +189,41 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
label = __("Serial Nos / Batch Nos");
|
||||
}
|
||||
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("{0} {1} via CSV File", [primary_label, label]),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.item?.has_serial_no) {
|
||||
fields = [
|
||||
...fields,
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: __("Import Using CSV file"),
|
||||
fieldname: "import_using_csv_file",
|
||||
default: 0,
|
||||
let fields = [];
|
||||
if (this.item.has_serial_no) {
|
||||
fields.push({
|
||||
fieldtype: "Check",
|
||||
label: __("Enter Manually"),
|
||||
fieldname: "enter_manually",
|
||||
default: 1,
|
||||
depends_on: "eval:doc.import_using_csv_file !== 1",
|
||||
change() {
|
||||
if (me.dialog.get_value("enter_manually")) {
|
||||
me.dialog.set_value("import_using_csv_file", 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("{0} {1} Manually", [primary_label, label]),
|
||||
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Enter Serial No Range"),
|
||||
fieldname: "serial_no_range",
|
||||
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||
description: __('Enter "ABC-001::100" for serial nos "ABC-001" to "ABC-100".'),
|
||||
onchange: () => {
|
||||
this.set_serial_nos_from_range();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
label: __("Enter Serial Nos"),
|
||||
fieldname: "upload_serial_nos",
|
||||
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||
description: __("Enter each serial no in a new line"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||
},
|
||||
{
|
||||
fieldtype: "Button",
|
||||
fieldname: "make_serial_nos",
|
||||
label: __("Create Serial Nos"),
|
||||
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||
click: () => {
|
||||
this.create_serial_nos();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
depends_on: "eval:doc.import_using_csv_file === 1",
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
fields = [
|
||||
...fields,
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: __("Import Using CSV file"),
|
||||
fieldname: "import_using_csv_file",
|
||||
depends_on: "eval:doc.enter_manually !== 1",
|
||||
default: !this.item.has_serial_no ? 1 : 0,
|
||||
change() {
|
||||
if (me.dialog.get_value("import_using_csv_file")) {
|
||||
me.dialog.set_value("enter_manually", 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
depends_on: "eval:doc.import_using_csv_file === 1",
|
||||
label: __("{0} {1} via CSV File", [primary_label, label]),
|
||||
},
|
||||
{
|
||||
fieldtype: "Button",
|
||||
fieldname: "download_csv",
|
||||
@@ -262,9 +241,51 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
},
|
||||
];
|
||||
|
||||
if (this.item?.has_serial_no) {
|
||||
fields = [
|
||||
...fields,
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("{0} {1} Manually", [primary_label, label]),
|
||||
depends_on: "eval:doc.enter_manually === 1",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Serial No Range"),
|
||||
fieldname: "serial_no_range",
|
||||
depends_on: "eval:doc.enter_manually === 1 && !doc.serial_no_series",
|
||||
description: __('"SN-01::10" for "SN-01" to "SN-10"'),
|
||||
onchange: () => {
|
||||
this.set_serial_nos_from_range();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (this.item?.has_serial_no) {
|
||||
fields = [
|
||||
...fields,
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
depends_on: "eval:doc.enter_manually === 1",
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
label: __("Enter Serial Nos"),
|
||||
fieldname: "upload_serial_nos",
|
||||
depends_on: "eval:doc.enter_manually === 1",
|
||||
description: __("Enter each serial no in a new line"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
set_serial_nos_from_series() {}
|
||||
|
||||
set_batch_nos_from_series() {}
|
||||
|
||||
set_serial_nos_from_range() {
|
||||
const serial_no_range = this.dialog.get_value("serial_no_range");
|
||||
|
||||
@@ -511,6 +532,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
scan_barcode_data() {
|
||||
const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
|
||||
|
||||
this.dialog.set_value("enter_manually", 0);
|
||||
|
||||
if (scan_serial_no || scan_batch_no) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists",
|
||||
@@ -554,14 +577,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
serial_no: scan_serial_no,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
this.dialog.fields_dict.entries.df.data.push({
|
||||
serial_no: scan_serial_no,
|
||||
batch_no: r.message,
|
||||
});
|
||||
this.dialog.fields_dict.entries.df.data.push({
|
||||
serial_no: scan_serial_no,
|
||||
batch_no: r.message,
|
||||
});
|
||||
|
||||
this.dialog.fields_dict.scan_serial_no.set_value("");
|
||||
}
|
||||
this.dialog.fields_dict.scan_serial_no.set_value("");
|
||||
this.dialog.fields_dict.entries.grid.refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -590,6 +612,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
update_bundle_entries() {
|
||||
let entries = this.dialog.get_values().entries;
|
||||
let warehouse = this.dialog.get_value("warehouse");
|
||||
let upload_serial_nos = this.dialog.get_value("upload_serial_nos");
|
||||
|
||||
if (!entries?.length && upload_serial_nos) {
|
||||
this.create_serial_nos();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((entries && !entries.length) || !entries) {
|
||||
frappe.throw(__("Please add atleast one Serial No / Batch No"));
|
||||
@@ -610,9 +638,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
this.callback && this.callback(r.message);
|
||||
this.frm.save();
|
||||
this.dialog.hide();
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
this.callback && this.callback(r.message);
|
||||
},
|
||||
() => this.frm.save(),
|
||||
() => this.dialog.hide(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -561,12 +561,50 @@ class TestQuotation(FrappeTestCase):
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 10,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
quotation.submit()
|
||||
|
||||
self.assertEqual(quotation.net_total, 290)
|
||||
self.assertEqual(quotation.grand_total, 319)
|
||||
self.assertEqual(round(quotation.items[1].net_rate, 2), 136.36)
|
||||
self.assertEqual(round(quotation.items[1].amount, 2), 150)
|
||||
|
||||
self.assertEqual(round(quotation.items[2].net_rate, 2), 163.64)
|
||||
self.assertEqual(round(quotation.items[2].amount, 2), 180)
|
||||
|
||||
self.assertEqual(round(quotation.net_total, 2), 263.64)
|
||||
self.assertEqual(round(quotation.total_taxes_and_charges, 2), 26.36)
|
||||
self.assertEqual(quotation.grand_total, 290)
|
||||
|
||||
def test_amount_calculation_for_alternative_items(self):
|
||||
"""Make sure that the amount is calculated correctly for alternative items when the qty is changed."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_list = []
|
||||
stock_items = {
|
||||
"_Test Simple Item 1": 100,
|
||||
"_Test Alt 1": 120,
|
||||
}
|
||||
|
||||
for item, rate in stock_items.items():
|
||||
make_item(item, {"is_stock_item": 0})
|
||||
item_list.append(
|
||||
{
|
||||
"item_code": item,
|
||||
"qty": 1,
|
||||
"rate": rate,
|
||||
"is_alternative": "Alt" in item,
|
||||
}
|
||||
)
|
||||
|
||||
quotation = make_quotation(item_list=item_list, do_not_submit=1)
|
||||
|
||||
self.assertEqual(quotation.items[1].amount, 120)
|
||||
|
||||
quotation.items[1].qty = 2
|
||||
quotation.save()
|
||||
|
||||
self.assertEqual(quotation.items[1].amount, 240)
|
||||
|
||||
def test_alternative_items_sales_order_mapping_with_stock_items(self):
|
||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||
|
||||
@@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs):
|
||||
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
|
||||
if test_exchange_values.get(kwargs["params"]["date"]):
|
||||
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
|
||||
elif args[0].startswith("https://frankfurter.app") and kwargs.get("params"):
|
||||
elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"):
|
||||
if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
|
||||
date = args[0].replace("https://frankfurter.app/", "")
|
||||
date = args[0].replace("https://api.frankfurter.app/", "")
|
||||
if test_exchange_values.get(date):
|
||||
return PatchResponse(
|
||||
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200
|
||||
|
||||
@@ -98,7 +98,7 @@ def setup_currency_exchange():
|
||||
ces.set("result_key", [])
|
||||
ces.set("req_params", [])
|
||||
|
||||
ces.api_endpoint = "https://frankfurter.app/{transaction_date}"
|
||||
ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}"
|
||||
ces.append("result_key", {"key": "rates"})
|
||||
ces.append("result_key", {"key": "{to_currency}"})
|
||||
ces.append("req_params", {"key": "base", "value": "{from_currency}"})
|
||||
|
||||
@@ -48,17 +48,18 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
let actual_qty = unescape(element.attr("data-actual_qty"));
|
||||
let disable_quick_entry = Number(unescape(element.attr("data-disable_quick_entry")));
|
||||
let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt";
|
||||
let stock_uom = unescape(element.attr("data-stock-uom"));
|
||||
|
||||
if (disable_quick_entry) {
|
||||
open_stock_entry(item, warehouse, entry_type);
|
||||
} else {
|
||||
if (action === "Add") {
|
||||
let rate = unescape($(this).attr("data-rate"));
|
||||
erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function () {
|
||||
erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, stock_uom, function () {
|
||||
me.refresh();
|
||||
});
|
||||
} else {
|
||||
erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function () {
|
||||
erpnext.stock.move_item(item, warehouse, null, actual_qty, null, stock_uom, function () {
|
||||
me.refresh();
|
||||
});
|
||||
}
|
||||
@@ -207,7 +208,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.stock.move_item = function (item, source, target, actual_qty, rate, callback) {
|
||||
erpnext.stock.move_item = function (item, source, target, actual_qty, rate, stock_uom, callback) {
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: target ? __("Add Item") : __("Move Item"),
|
||||
fields: [
|
||||
@@ -295,6 +296,8 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
|
||||
let row = frappe.model.add_child(doc, "items");
|
||||
row.item_code = dialog.get_value("item_code");
|
||||
row.s_warehouse = dialog.get_value("source");
|
||||
row.stock_uom = stock_uom;
|
||||
row.uom = stock_uom;
|
||||
row.t_warehouse = dialog.get_value("target");
|
||||
row.qty = dialog.get_value("qty");
|
||||
row.conversion_factor = 1;
|
||||
|
||||
@@ -71,6 +71,7 @@ def get_data(
|
||||
item.update(
|
||||
{
|
||||
"item_name": frappe.get_cached_value("Item", item.item_code, "item_name"),
|
||||
"stock_uom": frappe.get_cached_value("Item", item.item_code, "stock_uom"),
|
||||
"disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no")
|
||||
or frappe.get_cached_value("Item", item.item_code, "has_serial_no"),
|
||||
"projected_qty": flt(item.projected_qty, precision),
|
||||
|
||||
@@ -49,12 +49,14 @@
|
||||
data-disable_quick_entry="{{ d.disable_quick_entry }}"
|
||||
data-warehouse="{{ d.warehouse }}"
|
||||
data-actual_qty="{{ d.actual_qty }}"
|
||||
data-stock-uom="{{ d.stock_uom }}"
|
||||
data-item="{{ escape(d.item_code) }}">{{ __("Move") }}</a>
|
||||
{% endif %}
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add"
|
||||
data-disable_quick_entry="{{ d.disable_quick_entry }}"
|
||||
data-warehouse="{{ d.warehouse }}"
|
||||
data-actual_qty="{{ d.actual_qty }}"
|
||||
data-stock-uom="{{ d.stock_uom }}"
|
||||
data-item="{{ escape(d.item_code) }}"
|
||||
data-rate="{{ d.valuation_rate }}">{{ __("Add") }}</a>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ from frappe.utils import (
|
||||
strip_html,
|
||||
)
|
||||
from frappe.utils.html_utils import clean_html
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
from erpnext.controllers.item_variant import (
|
||||
@@ -345,7 +346,13 @@ class Item(Document):
|
||||
def validate_item_tax_net_rate_range(self):
|
||||
for tax in self.get("taxes"):
|
||||
if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
|
||||
frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate"))
|
||||
frappe.throw(
|
||||
_("Taxes row #{0}: {1} cannot be smaller than {2}").format(
|
||||
tax.idx,
|
||||
bold(_(tax.meta.get_label("maximum_net_rate"))),
|
||||
bold(_(tax.meta.get_label("minimum_net_rate"))),
|
||||
)
|
||||
)
|
||||
|
||||
def update_template_tables(self):
|
||||
template = frappe.get_cached_doc("Item", self.variant_of)
|
||||
@@ -1133,34 +1140,10 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
|
||||
"""returns last purchase details in stock uom"""
|
||||
# get last purchase order item details
|
||||
|
||||
last_purchase_order = frappe.db.sql(
|
||||
"""\
|
||||
select po.name, po.transaction_date, po.conversion_rate,
|
||||
po_item.conversion_factor, po_item.base_price_list_rate,
|
||||
po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate
|
||||
from `tabPurchase Order` po, `tabPurchase Order Item` po_item
|
||||
where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and
|
||||
po.name = po_item.parent
|
||||
order by po.transaction_date desc, po.name desc
|
||||
limit 1""",
|
||||
(item_code, cstr(doc_name)),
|
||||
as_dict=1,
|
||||
)
|
||||
last_purchase_order = get_purchase_voucher_details("Purchase Order", item_code, doc_name)
|
||||
|
||||
# get last purchase receipt item details
|
||||
last_purchase_receipt = frappe.db.sql(
|
||||
"""\
|
||||
select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate,
|
||||
pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage,
|
||||
pr_item.base_rate, pr_item.base_net_rate
|
||||
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
|
||||
where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and
|
||||
pr.name = pr_item.parent
|
||||
order by pr.posting_date desc, pr.posting_time desc, pr.name desc
|
||||
limit 1""",
|
||||
(item_code, cstr(doc_name)),
|
||||
as_dict=1,
|
||||
)
|
||||
last_purchase_receipt = get_purchase_voucher_details("Purchase Receipt", item_code, doc_name)
|
||||
|
||||
purchase_order_date = getdate(
|
||||
last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01"
|
||||
@@ -1181,7 +1164,13 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
|
||||
purchase_date = purchase_receipt_date
|
||||
|
||||
else:
|
||||
return frappe._dict()
|
||||
last_purchase_invoice = get_purchase_voucher_details("Purchase Invoice", item_code, doc_name)
|
||||
|
||||
if last_purchase_invoice:
|
||||
last_purchase = last_purchase_invoice[0]
|
||||
purchase_date = getdate(last_purchase.posting_date)
|
||||
else:
|
||||
return frappe._dict()
|
||||
|
||||
conversion_factor = flt(last_purchase.conversion_factor)
|
||||
out = frappe._dict(
|
||||
@@ -1207,6 +1196,40 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
|
||||
return out
|
||||
|
||||
|
||||
def get_purchase_voucher_details(doctype, item_code, document_name):
|
||||
parent_doc = frappe.qb.DocType(doctype)
|
||||
child_doc = frappe.qb.DocType(doctype + " Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(parent_doc)
|
||||
.inner_join(child_doc)
|
||||
.on(parent_doc.name == child_doc.parent)
|
||||
.select(
|
||||
parent_doc.name,
|
||||
parent_doc.conversion_rate,
|
||||
child_doc.conversion_factor,
|
||||
child_doc.base_price_list_rate,
|
||||
child_doc.discount_percentage,
|
||||
child_doc.base_rate,
|
||||
child_doc.base_net_rate,
|
||||
)
|
||||
.where(parent_doc.docstatus == 1)
|
||||
.where(child_doc.item_code == item_code)
|
||||
.where(parent_doc.name != document_name)
|
||||
)
|
||||
|
||||
if doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
query = query.select(parent_doc.posting_date, parent_doc.posting_time)
|
||||
query = query.orderby(
|
||||
parent_doc.posting_date, parent_doc.posting_time, parent_doc.name, order=Order.desc
|
||||
)
|
||||
else:
|
||||
query = query.select(parent_doc.transaction_date)
|
||||
query = query.orderby(parent_doc.transaction_date, parent_doc.name, order=Order.desc)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def check_stock_uom_with_bin(item, stock_uom):
|
||||
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
|
||||
return
|
||||
|
||||
@@ -245,6 +245,7 @@ class QualityInspection(Document):
|
||||
for i in range(1, 11):
|
||||
field = "reading_" + str(i)
|
||||
if reading.get(field) is None:
|
||||
data[field] = 0.0
|
||||
continue
|
||||
|
||||
data[field] = parse_float(reading.get(field))
|
||||
|
||||
@@ -280,7 +280,7 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
def validate_negative_batch(self, batch_no, available_qty):
|
||||
if available_qty < 0:
|
||||
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty):
|
||||
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
|
||||
has negative stock
|
||||
of quantity {bold(available_qty)} in the
|
||||
@@ -288,6 +288,18 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
frappe.throw(_(msg), BatchNegativeStockError)
|
||||
|
||||
def is_stock_reco_for_valuation_adjustment(self, available_qty):
|
||||
if (
|
||||
self.voucher_type == "Stock Reconciliation"
|
||||
and self.type_of_transaction == "Outward"
|
||||
and self.voucher_detail_no
|
||||
and abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty"))
|
||||
== abs(available_qty)
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_sle_for_outward_transaction(self):
|
||||
sle = frappe._dict(
|
||||
{
|
||||
@@ -1329,6 +1341,15 @@ def create_serial_batch_no_ledgers(
|
||||
}
|
||||
)
|
||||
|
||||
batch_no = None
|
||||
|
||||
if (
|
||||
not entries[0].get("batch_no")
|
||||
and entries[0].get("serial_no")
|
||||
and frappe.get_cached_value("Item", child_row.item_code, "has_batch_no")
|
||||
):
|
||||
batch_no = get_batch(child_row.item_code)
|
||||
|
||||
for row in entries:
|
||||
row = frappe._dict(row)
|
||||
doc.append(
|
||||
@@ -1336,7 +1357,7 @@ def create_serial_batch_no_ledgers(
|
||||
{
|
||||
"qty": (flt(row.qty) or 1.0) * (1 if type_of_transaction == "Inward" else -1),
|
||||
"warehouse": warehouse,
|
||||
"batch_no": row.batch_no,
|
||||
"batch_no": row.batch_no or batch_no,
|
||||
"serial_no": row.serial_no,
|
||||
},
|
||||
)
|
||||
@@ -1351,6 +1372,18 @@ def create_serial_batch_no_ledgers(
|
||||
return doc
|
||||
|
||||
|
||||
def get_batch(item_code):
|
||||
from erpnext.stock.doctype.batch.batch import make_batch
|
||||
|
||||
return make_batch(
|
||||
frappe._dict(
|
||||
{
|
||||
"item": item_code,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_type_of_transaction(parent_doc, child_row):
|
||||
type_of_transaction = child_row.get("type_of_transaction")
|
||||
if parent_doc.get("doctype") == "Stock Entry":
|
||||
|
||||
@@ -1594,10 +1594,6 @@ class StockEntry(StockController):
|
||||
if pro_doc.status == "Stopped":
|
||||
msg = f"Transaction not allowed against stopped Work Order {self.work_order}"
|
||||
|
||||
if self.is_return and pro_doc.status not in ["Completed", "Closed"]:
|
||||
title = _("Stock Return")
|
||||
msg = f"Work Order {self.work_order} must be completed or closed"
|
||||
|
||||
if msg:
|
||||
frappe.throw(_(msg), title=title)
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ class StockReconciliation(StockController):
|
||||
self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
|
||||
|
||||
def validate(self):
|
||||
self.validate_items_exist()
|
||||
if not self.expense_account:
|
||||
self.expense_account = frappe.get_cached_value(
|
||||
"Company", self.company, "stock_adjustment_account"
|
||||
@@ -162,6 +163,9 @@ class StockReconciliation(StockController):
|
||||
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
|
||||
"""Set Serial and Batch Bundle for each item"""
|
||||
for item in self.items:
|
||||
if not frappe.db.exists("Item", item.item_code):
|
||||
frappe.throw(_("Item {0} does not exist").format(item.item_code))
|
||||
|
||||
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
|
||||
bundle = self.get_bundle_for_specific_serial_batch(item)
|
||||
item.current_serial_and_batch_bundle = bundle.name
|
||||
@@ -357,6 +361,9 @@ class StockReconciliation(StockController):
|
||||
|
||||
def set_new_serial_and_batch_bundle(self):
|
||||
for item in self.items:
|
||||
if not item.item_code:
|
||||
continue
|
||||
|
||||
if item.use_serial_batch_fields:
|
||||
continue
|
||||
|
||||
@@ -684,7 +691,7 @@ class StockReconciliation(StockController):
|
||||
from erpnext.stock.stock_ledger import get_stock_value_difference
|
||||
|
||||
difference_amount = get_stock_value_difference(
|
||||
row.item_code, row.warehouse, self.posting_date, self.posting_time
|
||||
row.item_code, row.warehouse, self.posting_date, self.posting_time, self.name
|
||||
)
|
||||
|
||||
if not difference_amount:
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-01-14 10:52:58",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"delivery_document_type\", \"in\", [\"Delivery Note\", \"Sales Invoice\"]], [\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
|
||||
"modified": "2017-02-24 20:02:00.706889",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No Service Contract Expiry",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Serial No",
|
||||
"report_name": "Serial No Service Contract Expiry",
|
||||
"report_type": "Report Builder",
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-01-14 10:52:58",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
|
||||
"letterhead": null,
|
||||
"modified": "2024-09-26 13:07:23.451182",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No Service Contract Expiry",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Serial No",
|
||||
"report_name": "Serial No Service Contract Expiry",
|
||||
"report_type": "Report Builder",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Item Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-01-14 10:52:58",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 4,
|
||||
"is_standard": "Yes",
|
||||
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"], [\"purchase_document_type\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"purchase_rate\", \"Serial No\"], [\"delivery_document_type\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"]]}",
|
||||
"modified": "2017-02-24 19:54:21.392265",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No Status",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Serial No",
|
||||
"report_name": "Serial No Status",
|
||||
"report_type": "Report Builder",
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-01-14 10:52:58",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 4,
|
||||
"is_standard": "Yes",
|
||||
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"],[\"purchase_document_no\", \"Serial No\"]]}",
|
||||
"letterhead": null,
|
||||
"modified": "2024-09-26 13:10:52.693648",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No Status",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Serial No",
|
||||
"report_name": "Serial No Status",
|
||||
"report_type": "Report Builder",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Item Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-01-14 10:52:58",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"delivery_document_type\", \"in\", [\"Delivery Note\", \"Sales Invoice\"]], [\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warranty_expiry_date\", \"Serial No\"], [\"warranty_period\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
|
||||
"modified": "2017-02-24 20:01:53.097456",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No Warranty Expiry",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Serial No",
|
||||
"report_name": "Serial No Warranty Expiry",
|
||||
"report_type": "Report Builder",
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-01-14 10:52:58",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
|
||||
"letterhead": null,
|
||||
"modified": "2024-09-26 13:07:23.451182",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No Service Contract Expiry",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Serial No",
|
||||
"report_name": "Serial No Service Contract Expiry",
|
||||
"report_type": "Report Builder",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Item Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ def add_invariant_check_fields(sles):
|
||||
balance_qty = 0.0
|
||||
balance_stock_value = 0.0
|
||||
for idx, sle in enumerate(sles):
|
||||
queue = json.loads(sle.stock_queue)
|
||||
queue = json.loads(sle.stock_queue) if sle.stock_queue else []
|
||||
|
||||
fifo_qty = 0.0
|
||||
fifo_value = 0.0
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
|
||||
frappe.query_reports["Warehouse wise Item Balance Age and Value"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
@@ -39,6 +48,12 @@ frappe.query_reports["Warehouse wise Item Balance Age and Value"] = {
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Warehouse",
|
||||
get_query: function () {
|
||||
const company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: { company: company },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "filter_total_zero_qty",
|
||||
|
||||
@@ -109,8 +109,6 @@ def validate_filters(filters):
|
||||
sle_count = flt(frappe.qb.from_("Stock Ledger Entry").select(Count("name")).run()[0][0])
|
||||
if sle_count > 500000:
|
||||
frappe.throw(_("Please set filter based on Item or Warehouse"))
|
||||
if not filters.get("company"):
|
||||
filters["company"] = frappe.defaults.get_user_default("Company")
|
||||
|
||||
|
||||
def get_warehouse_list(filters):
|
||||
|
||||
@@ -865,7 +865,7 @@ class update_entries_after:
|
||||
sle.stock_value = self.wh_data.stock_value
|
||||
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
|
||||
|
||||
if not sle.is_adjustment_entry or not self.args.get("sle_id"):
|
||||
if not sle.is_adjustment_entry:
|
||||
sle.stock_value_difference = stock_value_difference
|
||||
|
||||
sle.doctype = "Stock Ledger Entry"
|
||||
|
||||
@@ -137,6 +137,11 @@ def task(doc_name, from_doctype, to_doctype):
|
||||
},
|
||||
"Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice},
|
||||
}
|
||||
|
||||
hooks = frappe.get_hooks("bulk_transaction_task_mapper")
|
||||
for hook in hooks:
|
||||
mapper.update(frappe.get_attr(hook)())
|
||||
|
||||
frappe.flags.bulk_transaction = True
|
||||
if to_doctype in ["Payment Entry"]:
|
||||
obj = mapper[from_doctype][to_doctype](from_doctype, doc_name)
|
||||
|
||||
Reference in New Issue
Block a user