Merge pull request #35222 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-05-10 11:08:42 +05:30
committed by GitHub
31 changed files with 433 additions and 63 deletions

View File

@@ -201,8 +201,11 @@ class Account(NestedSet):
)
def validate_account_currency(self):
self.currency_explicitly_specified = True
if not self.account_currency:
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
self.currency_explicitly_specified = False
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
@@ -248,8 +251,10 @@ class Account(NestedSet):
{
"company": company,
# parent account's currency should be passed down to child account's curreny
# if it is None, it picks it up from default company currency, which might be unintended
"account_currency": erpnext.get_company_currency(company),
# if currency explicitly specified by user, child will inherit. else, default currency will be used.
"account_currency": self.account_currency
if self.currency_explicitly_specified
else erpnext.get_company_currency(company),
"parent_account": parent_acc_name_map[company],
}
)

View File

@@ -5,10 +5,13 @@
import unittest
import frappe
from frappe.test_runner import make_test_records
from erpnext.accounts.doctype.account.account import merge_account, update_account_number
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"]
class TestAccount(unittest.TestCase):
def test_rename_account(self):
@@ -188,6 +191,58 @@ class TestAccount(unittest.TestCase):
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5")
def test_account_currency_sync(self):
"""
In a parent->child company setup, child should inherit parent account currency if explicitly specified.
"""
make_test_records("Company")
frappe.local.flags.pop("ignore_root_company_validation", None)
def create_bank_account():
acc = frappe.new_doc("Account")
acc.account_name = "_Test Bank JPY"
acc.parent_account = "Temporary Accounts - _TC6"
acc.company = "_Test Company 6"
return acc
acc = create_bank_account()
# Explicitly set currency
acc.account_currency = "JPY"
acc.insert()
self.assertTrue(
frappe.db.exists(
{
"doctype": "Account",
"account_name": "_Test Bank JPY",
"account_currency": "JPY",
"company": "_Test Company 7",
}
)
)
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
acc = create_bank_account()
# default currency is used
acc.insert()
self.assertTrue(
frappe.db.exists(
{
"doctype": "Account",
"account_name": "_Test Bank JPY",
"account_currency": "USD",
"company": "_Test Company 7",
}
)
)
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
def test_child_company_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)

View File

@@ -164,7 +164,7 @@ def trigger_reconciliation_for_queued_docs():
Fetch queued docs and start reconciliation process for each one
"""
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw(
frappe.msgprint(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
get_link_to_form("Accounts Settings", "Accounts Settings")
)

View File

@@ -1369,6 +1369,7 @@
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
"ignore_user_permissions": 1,
"width": "50px"
},
{
@@ -1572,7 +1573,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-04-28 12:57:50.832598",
"modified": "2023-04-29 12:57:50.832598",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -1171,6 +1171,7 @@
"depends_on": "is_internal_supplier",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Set From Warehouse",
"options": "Warehouse"
},
@@ -1271,7 +1272,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2023-04-14 16:42:29.448464",
"modified": "2023-05-07 20:18:09.196799",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -33,7 +33,6 @@ class Opportunity(TransactionBase, CRMNote):
def after_insert(self):
if self.opportunity_from == "Lead":
frappe.get_doc("Lead", self.party_name).set_status(update=True)
self.disable_lead()
link_open_tasks(self.opportunity_from, self.party_name, self)
link_open_events(self.opportunity_from, self.party_name, self)
@@ -119,10 +118,6 @@ class Opportunity(TransactionBase, CRMNote):
prospect.flags.ignore_mandatory = True
prospect.save()
def disable_lead(self):
if self.opportunity_from == "Lead":
frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1})
def make_new_lead_if_required(self):
"""Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email:

View File

@@ -48,7 +48,9 @@ frappe.ui.form.on("BOM", {
return {
query: "erpnext.manufacturing.doctype.bom.bom.item_query",
filters: {
"item_code": doc.item
"item_code": doc.item,
"include_item_in_manufacturing": 1,
"is_fixed_asset": 0
}
};
});

View File

@@ -1339,8 +1339,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not has_variants:
query_filters["has_variants"] = 0
if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1
if filters:
for fieldname, value in filters.items():
query_filters[fieldname] = value
return frappe.get_list(
"Item",

View File

@@ -698,6 +698,45 @@ class TestBOM(FrappeTestCase):
bom.update_cost()
self.assertFalse(bom.flags.cost_updated)
def test_do_not_include_manufacturing_and_fixed_items(self):
from erpnext.manufacturing.doctype.bom.bom import item_query
if not frappe.db.exists("Asset Category", "Computers-Test"):
doc = frappe.get_doc({"doctype": "Asset Category", "asset_category_name": "Computers-Test"})
doc.flags.ignore_mandatory = True
doc.insert()
for item_code, properties in {
"_Test RM Item 1 Do Not Include In Manufacture": {
"is_stock_item": 1,
"include_item_in_manufacturing": 0,
},
"_Test RM Item 2 Fixed Asset Item": {
"is_fixed_asset": 1,
"is_stock_item": 0,
"asset_category": "Computers-Test",
},
"_Test RM Item 3 Manufacture Item": {"is_stock_item": 1, "include_item_in_manufacturing": 1},
}.items():
make_item(item_code, properties)
data = item_query(
"Item",
txt="_Test RM Item",
searchfield="name",
start=0,
page_len=20000,
filters={"include_item_in_manufacturing": 1, "is_fixed_asset": 0},
)
items = []
for row in data:
items.append(row[0])
self.assertTrue("_Test RM Item 1 Do Not Include In Manufacture" not in items)
self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items)
self.assertTrue("_Test RM Item 3 Manufacture Item" in items)
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@@ -16,6 +16,7 @@
"column_break_4",
"quantity",
"uom",
"conversion_factor",
"projected_qty",
"reserved_qty_for_production",
"safety_stock",
@@ -169,11 +170,17 @@
"label": "Qty As Per BOM",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "Conversion Factor",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2022-11-26 14:59:25.879631",
"modified": "2023-05-03 12:43:29.895754",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Request Plan Item",

View File

@@ -336,10 +336,6 @@ frappe.ui.form.on('Production Plan', {
},
get_items_for_material_requests(frm, warehouses) {
let set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse',
'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty',
'reserved_qty_for_production', 'material_request_type'];
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
freeze: true,
@@ -352,11 +348,11 @@ frappe.ui.form.on('Production Plan', {
frm.set_value('mr_items', []);
r.message.forEach(row => {
let d = frm.add_child('mr_items');
set_fields.forEach(field => {
if (row[field]) {
for (let field in row) {
if (field !== 'name') {
d[field] = row[field];
}
});
}
});
}
refresh_field('mr_items');

View File

@@ -28,6 +28,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_or_make_bin
from erpnext.utilities.transaction_base import validate_uom_is_integer
@@ -398,9 +399,20 @@ class ProductionPlan(Document):
self.set_status()
self.db_set("status", self.status)
def on_submit(self):
self.update_bin_qty()
def on_cancel(self):
self.db_set("status", "Cancelled")
self.delete_draft_work_order()
self.update_bin_qty()
def update_bin_qty(self):
for d in self.mr_items:
if d.warehouse:
bin_name = get_or_make_bin(d.item_code, d.warehouse)
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_production_plan()
def delete_draft_work_order(self):
for d in frappe.get_all(
@@ -575,6 +587,7 @@ class ProductionPlan(Document):
"production_plan_sub_assembly_item": row.name,
"bom": row.bom_no,
"production_plan": self.name,
"fg_item_qty": row.qty,
}
for field in [
@@ -1068,6 +1081,7 @@ def get_material_request_items(
"item_code": row.item_code,
"item_name": row.item_name,
"quantity": required_qty / conversion_factor,
"conversion_factor": conversion_factor,
"required_bom_qty": total_qty,
"stock_uom": row.get("stock_uom"),
"warehouse": warehouse
@@ -1474,3 +1488,34 @@ def set_default_warehouses(row, default_warehouses):
for field in ["wip_warehouse", "fg_warehouse"]:
if not row.get(field):
row[field] = default_warehouses.get(field)
def get_reserved_qty_for_production_plan(item_code, warehouse):
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Material Request Plan Item")
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0)))
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
& (child.warehouse == warehouse)
& (table.status.notin(["Completed", "Closed"]))
)
).run()
if not query:
return 0.0
reserved_qty_for_production_plan = flt(query[0][0])
reserved_qty_for_production = flt(
get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True)
)
return reserved_qty_for_production_plan - reserved_qty_for_production

View File

@@ -307,6 +307,43 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
def test_production_plan_for_subcontracting_po(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
bom_tree_1 = {"Test Laptop 1": {"Test Motherboard 1": {"Test Motherboard Wires 1": {}}}}
create_nested_bom(bom_tree_1, prefix="")
item_doc = frappe.get_doc("Item", "Test Motherboard 1")
company = "_Test Company"
item_doc.is_sub_contracted_item = 1
for row in item_doc.item_defaults:
if row.company == company and not row.default_supplier:
row.default_supplier = "_Test Supplier"
if not item_doc.item_defaults:
item_doc.append("item_defaults", {"company": company, "default_supplier": "_Test Supplier"})
item_doc.save()
plan = create_production_plan(
item_code="Test Laptop 1", planned_qty=10, use_multi_level_bom=1, do_not_submit=True
)
plan.get_sub_assembly_items()
plan.set_default_supplier_for_subcontracting_order()
plan.submit()
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
plan.make_work_order()
po = frappe.db.get_value("Purchase Order Item", {"production_plan": plan.name}, "parent")
po_doc = frappe.get_doc("Purchase Order", po)
self.assertEqual(po_doc.supplier, "_Test Supplier")
self.assertEqual(po_doc.items[0].qty, 10.0)
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
self.assertEqual(po_doc.items[0].fg_item, "Test Motherboard 1")
def test_production_plan_combine_subassembly(self):
"""
Test combining Sub assembly items belonging to the same BOM in Prod Plan.
@@ -868,6 +905,27 @@ class TestProductionPlan(FrappeTestCase):
for item_code in mr_items:
self.assertTrue(item_code in validate_mr_items)
def test_resered_qty_for_production_plan_for_material_requests(self):
from erpnext.stock.utils import get_or_make_bin
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln = create_production_plan(item_code="Test Production Item 1")
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty - before_qty, 1)
pln = frappe.get_doc("Production Plan", pln.name)
pln.cancel()
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty, before_qty)
def create_production_plan(**args):
"""

View File

@@ -558,12 +558,19 @@ class WorkOrder(Document):
and self.production_plan_item
and not self.production_plan_sub_assembly_item
):
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
table = frappe.qb.DocType("Work Order")
if self.docstatus == 1:
qty += self.qty
elif self.docstatus == 2:
qty -= self.qty
query = (
frappe.qb.from_(table)
.select(Sum(table.qty))
.where(
(table.production_plan == self.production_plan)
& (table.production_plan_item == self.production_plan_item)
& (table.docstatus == 1)
)
).run()
qty = flt(query[0][0]) if query else 0
frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty)
@@ -1476,12 +1483,14 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
return doc
def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
def get_reserved_qty_for_production(
item_code: str, warehouse: str, check_production_plan: bool = False
) -> float:
"""Get total reserved quantity for any item in specified warehouse"""
wo = frappe.qb.DocType("Work Order")
wo_item = frappe.qb.DocType("Work Order Item")
return (
query = (
frappe.qb.from_(wo)
.from_(wo_item)
.select(
@@ -1502,7 +1511,12 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
| (wo_item.required_qty > wo_item.consumed_qty)
)
)
).run()[0][0] or 0.0
)
if check_production_plan:
query = query.where(wo.production_plan.isnotnull())
return query.run()[0][0] or 0.0
@frappe.whitelist()

View File

@@ -326,6 +326,7 @@ erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v14_0.set_pick_list_status
erpnext.patches.v13_0.update_docs_link
erpnext.patches.v14_0.enable_all_leads
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
# below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger

View File

@@ -0,0 +1,8 @@
import frappe
def execute():
lead = frappe.qb.DocType("Lead")
frappe.qb.update(lead).set(lead.disabled, 0).set(lead.docstatus, 0).where(
lead.disabled == 1 and lead.docstatus == 1
).run()

View File

@@ -92,7 +92,7 @@ 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.frm._items = is_quotation ? this.filtered_items() : this.frm.doc.items;
this.validate_conversion_rate();
this.calculate_item_values();
@@ -125,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._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;
@@ -209,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._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;
@@ -280,13 +280,13 @@ 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._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;
me.frm.doc.net_total += item.net_amount;
me.frm.doc.base_net_total += item.base_net_amount;
});
});
}
calculate_shipping_charges() {
@@ -333,7 +333,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
});
$.each(this.frm.doc._items || [], function(n, item) {
$.each(this.frm._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
@@ -342,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._items.length - 1) {
current_tax_amount += actual_tax_dict[tax.idx];
}
}
@@ -379,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._items.length - 1) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
@@ -602,7 +602,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.doc._items;
let items = this.frm._items;
if(items && items.length) {
if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
@@ -659,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._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));
@@ -667,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._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,

View File

@@ -665,11 +665,15 @@ def get_credit_limit(customer, company):
if not credit_limit:
customer_group = frappe.get_cached_value("Customer", customer, "customer_group")
credit_limit = frappe.db.get_value(
result = frappe.db.get_values(
"Customer Credit Limit",
{"parent": customer_group, "parenttype": "Customer Group", "company": company},
"credit_limit",
fieldname=["credit_limit", "bypass_credit_limit_check"],
as_dict=True,
)
if result and not result[0].bypass_credit_limit_check:
credit_limit = result[0].credit_limit
if not credit_limit:
credit_limit = frappe.get_cached_value("Company", company, "credit_limit")

View File

@@ -286,6 +286,18 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
target.commission_rate = frappe.get_value(
"Sales Partner", source.referral_sales_partner, "commission_rate"
)
# sales team
for d in customer.get("sales_team"):
target.append(
"sales_team",
{
"sales_person": d.sales_person,
"allocated_percentage": d.allocated_percentage or None,
"commission_rate": d.commission_rate,
},
)
target.flags.ignore_permissions = ignore_permissions
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")

View File

@@ -80,5 +80,30 @@
"chart_of_accounts": "Standard",
"enable_perpetual_inventory": 1,
"default_holiday_list": "_Test Holiday List"
},
{
"abbr": "_TC6",
"company_name": "_Test Company 6",
"is_group": 1,
"country": "India",
"default_currency": "INR",
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
},
{
"abbr": "_TC7",
"company_name": "_Test Company 7",
"parent_company": "_Test Company 6",
"is_group": 1,
"country": "United States",
"default_currency": "USD",
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
}
]

View File

@@ -15,6 +15,7 @@
"projected_qty",
"reserved_qty_for_production",
"reserved_qty_for_sub_contract",
"reserved_qty_for_production_plan",
"ma_rate",
"stock_uom",
"fcfs_rate",
@@ -165,13 +166,19 @@
"oldfieldname": "stock_value",
"oldfieldtype": "Currency",
"read_only": 1
},
{
"fieldname": "reserved_qty_for_production_plan",
"fieldtype": "Float",
"label": "Reserved Qty for Production Plan",
"read_only": 1
}
],
"hide_toolbar": 1,
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2022-03-30 07:22:23.868602",
"modified": "2023-05-02 23:26:21.806965",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",

View File

@@ -24,8 +24,30 @@ class Bin(Document):
- flt(self.reserved_qty)
- flt(self.reserved_qty_for_production)
- flt(self.reserved_qty_for_sub_contract)
- flt(self.reserved_qty_for_production_plan)
)
def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False):
"""Update qty reserved for production from Production Plan tables
in open production plan"""
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_reserved_qty_for_production_plan,
)
self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
self.item_code, self.warehouse
)
self.db_set(
"reserved_qty_for_production_plan",
flt(self.reserved_qty_for_production_plan),
update_modified=True,
)
if not skip_project_qty_update:
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
def update_reserved_qty_for_production(self):
"""Update qty reserved for production from Production Item tables
in open work orders"""
@@ -35,11 +57,13 @@ class Bin(Document):
self.item_code, self.warehouse
)
self.set_projected_qty()
self.db_set(
"reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True
)
self.update_reserved_qty_for_production_plan(skip_project_qty_update=True)
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"):
@@ -141,6 +165,7 @@ def get_bin_details(bin_name):
"planned_qty",
"reserved_qty_for_production",
"reserved_qty_for_sub_contract",
"reserved_qty_for_production_plan",
],
as_dict=1,
)
@@ -188,6 +213,7 @@ def update_qty(bin_name, args):
- flt(reserved_qty)
- flt(bin_details.reserved_qty_for_production)
- flt(bin_details.reserved_qty_for_sub_contract)
- flt(bin_details.reserved_qty_for_production_plan)
)
frappe.db.set_value(

View File

@@ -280,6 +280,7 @@
{
"fieldname": "set_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Set Target Warehouse",
"options": "Warehouse"
@@ -355,7 +356,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2022-09-27 17:58:26.366469",
"modified": "2023-05-07 20:17:29.108095",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",

View File

@@ -419,6 +419,7 @@
"depends_on": "eval:parent.material_request_type == \"Material Transfer\"",
"fieldname": "from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Source Warehouse",
"options": "Warehouse"
},
@@ -451,16 +452,16 @@
"fieldname": "job_card_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Job Card Item",
"no_copy": 1,
"print_hide": 1,
"label": "Job Card Item"
"print_hide": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-03-10 18:42:42.705190",
"modified": "2023-05-07 20:23:31.250252",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",
@@ -469,5 +470,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1113,6 +1113,7 @@
"depends_on": "eval: doc.is_internal_supplier",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Set From Warehouse",
"options": "Warehouse"
},
@@ -1238,7 +1239,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2022-12-12 18:40:32.447752",
"modified": "2023-05-07 20:18:25.458185",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -300,9 +300,7 @@ def _get_directly_dependent_vouchers(doc):
def notify_error_to_stock_managers(doc, traceback):
recipients = get_users_with_role("Stock Manager")
if not recipients:
recipients = get_users_with_role("System Manager")
recipients = get_recipients()
subject = _("Error while reposting item valuation")
message = (
@@ -319,6 +317,17 @@ def notify_error_to_stock_managers(doc, traceback):
frappe.sendmail(recipients=recipients, subject=subject, message=message)
def get_recipients():
role = (
frappe.db.get_single_value("Stock Reposting Settings", "notify_reposting_error_to_role")
or "Stock Manager"
)
recipients = get_users_with_role(role)
return recipients
def repost_entries():
"""
Reposts 'Repost Item Valuation' entries in queue.
@@ -344,7 +353,7 @@ def get_repost_item_valuation_entries():
return frappe.db.sql(
""" SELECT name from `tabRepost Item Valuation`
WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
ORDER BY timestamp(posting_date, posting_time) asc, creation asc, status asc
""",
now(),
as_dict=1,

View File

@@ -395,7 +395,8 @@
"no_copy": 1,
"options": "Material Request",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "material_request_item",
@@ -571,7 +572,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-01-03 14:51:16.575515",
"modified": "2023-05-09 12:41:18.210864",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -99,7 +99,7 @@
},
{
"fieldname": "serial_no",
"fieldtype": "Small Text",
"fieldtype": "Long Text",
"label": "Serial No"
},
{
@@ -120,7 +120,7 @@
},
{
"fieldname": "current_serial_no",
"fieldtype": "Small Text",
"fieldtype": "Long Text",
"label": "Current Serial No",
"no_copy": 1,
"print_hide": 1,
@@ -189,7 +189,7 @@
],
"istable": 1,
"links": [],
"modified": "2022-11-02 13:01:23.580937",
"modified": "2023-05-09 18:42:19.224916",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",

View File

@@ -12,7 +12,9 @@
"start_time",
"end_time",
"limits_dont_apply_on",
"item_based_reposting"
"item_based_reposting",
"errors_notification_section",
"notify_reposting_error_to_role"
],
"fields": [
{
@@ -52,12 +54,23 @@
"fieldname": "item_based_reposting",
"fieldtype": "Check",
"label": "Use Item based reposting"
},
{
"fieldname": "notify_reposting_error_to_role",
"fieldtype": "Link",
"label": "Notify Reposting Error to Role",
"options": "Role"
},
{
"fieldname": "errors_notification_section",
"fieldtype": "Section Break",
"label": "Errors Notification"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-11-02 01:22:45.155841",
"modified": "2023-05-04 16:14:29.080697",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",
@@ -76,5 +89,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,9 +1,40 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
import frappe
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import get_recipients
class TestStockRepostingSettings(unittest.TestCase):
pass
def test_notify_reposting_error_to_role(self):
role = "Notify Reposting Role"
if not frappe.db.exists("Role", role):
frappe.get_doc({"doctype": "Role", "role_name": role}).insert(ignore_permissions=True)
user = "notify_reposting_error@test.com"
if not frappe.db.exists("User", user):
frappe.get_doc(
{
"doctype": "User",
"email": user,
"first_name": "Test",
"language": "en",
"time_zone": "Asia/Kolkata",
"send_welcome_email": 0,
"roles": [{"role": role}],
}
).insert(ignore_permissions=True)
frappe.db.set_single_value("Stock Reposting Settings", "notify_reposting_error_to_role", "")
users = get_recipients()
self.assertFalse(user in users)
frappe.db.set_single_value("Stock Reposting Settings", "notify_reposting_error_to_role", role)
users = get_recipients()
self.assertTrue(user in users)

View File

@@ -76,6 +76,7 @@ def execute(filters=None):
bin.ordered_qty,
bin.reserved_qty,
bin.reserved_qty_for_production,
bin.reserved_qty_for_production_plan,
bin.reserved_qty_for_sub_contract,
reserved_qty_for_pos,
bin.projected_qty,
@@ -173,6 +174,13 @@ def get_columns():
"width": 100,
"convertible": "qty",
},
{
"label": _("Reserved for Production Plan"),
"fieldname": "reserved_qty_for_production_plan",
"fieldtype": "Float",
"width": 100,
"convertible": "qty",
},
{
"label": _("Reserved for Sub Contracting"),
"fieldname": "reserved_qty_for_sub_contract",
@@ -232,6 +240,7 @@ def get_bin_list(filters):
bin.reserved_qty,
bin.reserved_qty_for_production,
bin.reserved_qty_for_sub_contract,
bin.reserved_qty_for_production_plan,
bin.projected_qty,
)
.orderby(bin.item_code, bin.warehouse)