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

chore: release v14
This commit is contained in:
ruthra kumar
2023-02-28 18:57:57 +05:30
committed by GitHub
26 changed files with 340 additions and 140 deletions

View File

@@ -29,6 +29,7 @@ def create_charts(
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
account_number = cstr(child.get("account_number")).strip()
@@ -95,7 +96,17 @@ def identify_is_group(child):
is_group = child.get("is_group")
elif len(
set(child.keys())
- set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
- set(
[
"account_name",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_number",
"account_currency",
]
)
):
is_group = 1
else:
@@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
"root_type",
"tax_rate",
"account_number",
"account_currency",
],
order_by="lft, rgt",
)
@@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
continue

View File

@@ -36,7 +36,7 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data])
if no_of_columns > 7:
if no_of_columns > 8:
frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")),
@@ -233,6 +233,7 @@ def build_forest(data):
is_group,
account_type,
root_type,
account_currency,
) = i
if not account_name:
@@ -253,6 +254,8 @@ def build_forest(data):
charts_map[account_name]["account_type"] = account_type
if root_type:
charts_map[account_name]["root_type"] = root_type
if account_currency:
charts_map[account_name]["account_currency"] = account_currency
path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created
line_no += 1
@@ -315,6 +318,7 @@ def get_template(template_type):
"Is Group",
"Account Type",
"Root Type",
"Account Currency",
]
writer = UnicodeWriter()
writer.writerow(fields)

View File

@@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(
abs(available_batch_qty - reserved_batch_qty - item.qty)
abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
)
bold_invalid_batch_no = frappe.bold(item.batch_no)
@@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice):
).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"),
)
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
frappe.throw(
_(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
),
title=_("Item Unavailable"),
)
elif is_stock_item and flt(available_stock) < flt(d.qty):
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@@ -650,7 +650,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
max_available_bundles = available_qty / item.stock_qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):

View File

@@ -1047,7 +1047,7 @@ var select_loyalty_program = function(frm, loyalty_programs) {
]
});
dialog.set_primary_action(__("Set"), function() {
dialog.set_primary_action(__("Set Loyalty Program"), function() {
dialog.hide();
return frappe.call({
method: "frappe.client.set_value",

View File

@@ -395,6 +395,7 @@ def get_column_names():
class GrossProfitGenerator(object):
def __init__(self, filters=None):
self.sle = {}
self.data = []
self.average_buying_rate = {}
self.filters = frappe._dict(filters)
@@ -404,7 +405,6 @@ class GrossProfitGenerator(object):
if filters.group_by == "Invoice":
self.group_items_by_invoice()
self.load_stock_ledger_entries()
self.load_product_bundle()
self.load_non_stock_items()
self.get_returned_invoice_items()
@@ -633,7 +633,7 @@ class GrossProfitGenerator(object):
return flt(row.qty) * item_rate
else:
my_sle = self.sle.get((item_code, row.warehouse))
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent
if row.dn_detail:
@@ -651,7 +651,7 @@ class GrossProfitGenerator(object):
dn["item_row"],
dn["warehouse"],
)
my_sle = self.sle.get((item_code, warehouse))
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
@@ -667,15 +667,12 @@ class GrossProfitGenerator(object):
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
frappe.qb.from_(delivery_note)
.inner_join(delivery_note_item)
.on(delivery_note.name == delivery_note_item.parent)
frappe.qb.from_(delivery_note_item)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
.where(delivery_note.docstatus == 1)
.where(delivery_note_item.docstatus == 1)
.where(delivery_note_item.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail)
@@ -940,24 +937,36 @@ class GrossProfitGenerator(object):
"Item", item_code, ["item_name", "description", "item_group", "brand"]
)
def load_stock_ledger_entries(self):
res = frappe.db.sql(
"""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry`
where company=%(company)s and is_cancelled = 0
order by
item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""",
self.filters,
as_dict=True,
)
self.sle = {}
for r in res:
if (r.item_code, r.warehouse) not in self.sle:
self.sle[(r.item_code, r.warehouse)] = []
def get_stock_ledger_entries(self, item_code, warehouse):
if item_code and warehouse:
if (item_code, warehouse) not in self.sle:
sle = qb.DocType("Stock Ledger Entry")
res = (
qb.from_(sle)
.select(
sle.item_code,
sle.voucher_type,
sle.voucher_no,
sle.voucher_detail_no,
sle.stock_value,
sle.warehouse,
sle.actual_qty.as_("qty"),
)
.where(
(sle.company == self.filters.company)
& (sle.item_code == item_code)
& (sle.warehouse == warehouse)
& (sle.is_cancelled == 0)
)
.orderby(sle.item_code)
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
.run(as_dict=True)
)
self.sle[(r.item_code, r.warehouse)].append(r)
self.sle[(item_code, warehouse)] = res
return self.sle[(item_code, warehouse)]
return []
def load_product_bundle(self):
self.product_bundles = {}

View File

@@ -302,10 +302,6 @@ frappe.ui.form.on('Asset', {
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
},
opening_accumulated_depreciation: function(frm) {
erpnext.asset.set_accumulated_depreciation(frm);
},
make_schedules_editable: function(frm) {
if (frm.doc.finance_books) {
var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
@@ -567,19 +563,23 @@ frappe.ui.form.on('Depreciation Schedule', {
},
depreciation_amount: function(frm, cdt, cdn) {
erpnext.asset.set_accumulated_depreciation(frm);
erpnext.asset.set_accumulated_depreciation(frm, locals[cdt][cdn].finance_book_id);
}
})
});
erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return;
erpnext.asset.set_accumulated_depreciation = function(frm, finance_book_id) {
var depreciation_method = frm.doc.finance_books[Number(finance_book_id) - 1].depreciation_method;
if(depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name,
"accumulated_depreciation_amount", accumulated_depreciation);
if (row.finance_book_id === finance_book_id) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
};
})
};

View File

@@ -84,14 +84,55 @@ class Asset(AccountsController):
if self.calculate_depreciation:
self.value_after_depreciation = 0
self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_disposal)
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
if self.should_prepare_depreciation_schedule():
self.make_depreciation_schedule(date_of_disposal)
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
else:
self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation
)
def should_prepare_depreciation_schedule(self):
if not self.get("schedules"):
return True
old_asset_doc = self.get_doc_before_save()
if not old_asset_doc:
return True
have_asset_details_been_modified = (
old_asset_doc.gross_purchase_amount != self.gross_purchase_amount
or old_asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
or old_asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
)
if have_asset_details_been_modified:
return True
manual_fb_idx = -1
for d in self.finance_books:
if d.depreciation_method == "Manual":
manual_fb_idx = d.idx - 1
no_manual_depr_or_have_manual_depr_details_been_modified = (
manual_fb_idx == -1
or old_asset_doc.finance_books[manual_fb_idx].total_number_of_depreciations
!= self.finance_books[manual_fb_idx].total_number_of_depreciations
or old_asset_doc.finance_books[manual_fb_idx].frequency_of_depreciation
!= self.finance_books[manual_fb_idx].frequency_of_depreciation
or old_asset_doc.finance_books[manual_fb_idx].depreciation_start_date
!= getdate(self.finance_books[manual_fb_idx].depreciation_start_date)
or old_asset_doc.finance_books[manual_fb_idx].expected_value_after_useful_life
!= self.finance_books[manual_fb_idx].expected_value_after_useful_life
)
if no_manual_depr_or_have_manual_depr_details_been_modified:
return True
return False
def validate_item(self):
item = frappe.get_cached_value(
"Item", self.item_code, ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1
@@ -225,9 +266,7 @@ class Asset(AccountsController):
)
def make_depreciation_schedule(self, date_of_disposal):
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
"schedules"
):
if not self.get("schedules"):
self.schedules = []
if not self.available_for_use_date:
@@ -555,9 +594,7 @@ class Asset(AccountsController):
def set_accumulated_depreciation(
self, date_of_disposal=None, date_of_return=None, ignore_booked_entry=False
):
straight_line_idx = [
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
]
straight_line_idx = []
finance_books = []
for i, d in enumerate(self.get("schedules")):
@@ -565,6 +602,12 @@ class Asset(AccountsController):
continue
if int(d.finance_book_id) not in finance_books:
straight_line_idx = [
s.idx
for s in self.get("schedules")
if s.finance_book_id == d.finance_book_id
and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual")
]
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation

View File

@@ -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
},
]

View File

@@ -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
},
]

View File

@@ -64,8 +64,6 @@
"fieldtype": "Section Break"
},
{
"fetch_from": "prevdoc_detail_docname.sales_person",
"fetch_if_empty": 1,
"fieldname": "service_person",
"fieldtype": "Link",
"in_list_view": 1,
@@ -110,13 +108,15 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-05-27 17:47:21.474282",
"modified": "2023-02-27 11:09:33.114458",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit Purpose",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = {
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.id == "item") {
if (data["enough_parts_to_build"] > 0) {
if (data["in_stock_qty"] >= data["required_qty"]) {
value = `<a style='color:green' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else {
value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;

View File

@@ -131,8 +131,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));
}

View File

@@ -309,9 +309,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_work_order() {
var me = this;
this.frm.call({
doc: this.frm.doc,
method: 'get_work_order_items',
me.frm.call({
method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
args: {
sales_order: this.frm.docname,
},
freeze: true,
callback: function(r) {
if(!r.message) {
frappe.msgprint({
@@ -321,14 +324,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
});
return;
}
else if(!r.message) {
frappe.msgprint({
title: __('Work Order not created'),
message: __('Work Order already created for all items with BOM'),
indicator: 'orange'
});
return;
} else {
else {
const fields = [{
label: 'Items',
fieldtype: 'Table',
@@ -429,9 +425,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_raw_material_request() {
var me = this;
this.frm.call({
doc: this.frm.doc,
method: 'get_work_order_items',
method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
args: {
sales_order: this.frm.docname,
for_raw_material_request: 1
},
callback: function(r) {
@@ -450,6 +446,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
make_raw_material_request_dialog(r) {
var me = this;
var fields = [
{fieldtype:'Check', fieldname:'include_exploded_items',
label: __('Include Exploded Items')},

View File

@@ -6,11 +6,12 @@ import json
import frappe
import frappe.utils
from frappe import _
from frappe import _, qb
from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
@@ -414,51 +415,6 @@ class SalesOrder(SellingController):
self.indicator_color = "green"
self.indicator_title = _("Paid")
@frappe.whitelist()
def get_work_order_items(self, for_raw_material_request=0):
"""Returns items with BOM that already do not have a linked work order"""
items = []
item_codes = [i.item_code for i in self.items]
product_bundle_parents = [
pb.new_item_code
for pb in frappe.get_all(
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
)
]
for table in [self.items, self.packed_items]:
for i in table:
bom = get_default_bom(i.item_code)
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
if not for_raw_material_request:
total_work_order_qty = flt(
frappe.db.sql(
"""select sum(qty) from `tabWork Order`
where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""",
(i.item_code, self.name, i.name),
)[0][0]
)
pending_qty = stock_qty - total_work_order_qty
else:
pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents:
items.append(
dict(
name=i.name,
item_code=i.item_code,
description=i.description,
bom=bom or "",
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,
sales_order_item=i.name,
)
)
return items
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
@@ -1350,3 +1306,57 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
return
frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty)
@frappe.whitelist()
def get_work_order_items(sales_order, for_raw_material_request=0):
"""Returns items with BOM that already do not have a linked work order"""
if sales_order:
so = frappe.get_doc("Sales Order", sales_order)
wo = qb.DocType("Work Order")
items = []
item_codes = [i.item_code for i in so.items]
product_bundle_parents = [
pb.new_item_code
for pb in frappe.get_all(
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
)
]
for table in [so.items, so.packed_items]:
for i in table:
bom = get_default_bom(i.item_code)
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
if not for_raw_material_request:
total_work_order_qty = flt(
qb.from_(wo)
.select(Sum(wo.qty))
.where(
(wo.production_item == i.item_code)
& (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
& (wo.docstatus.lte(2))
)
.run()[0][0]
)
pending_qty = stock_qty - total_work_order_qty
else:
pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents:
items.append(
dict(
name=i.name,
item_code=i.item_code,
description=i.description,
bom=bom or "",
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,
sales_order_item=i.name,
)
)
return items

View File

@@ -1217,6 +1217,8 @@ class TestSalesOrder(FrappeTestCase):
self.assertTrue(si.get("payment_schedule"))
def test_make_work_order(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
# Make a new Sales Order
so = make_sales_order(
**{
@@ -1230,7 +1232,7 @@ class TestSalesOrder(FrappeTestCase):
# Raise Work Orders
po_items = []
so_item_name = {}
for item in so.get_work_order_items():
for item in get_work_order_items(so.name):
po_items.append(
{
"warehouse": item.get("warehouse"),
@@ -1448,6 +1450,7 @@ class TestSalesOrder(FrappeTestCase):
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
make_item( # template item
"Test-WO-Tshirt",
@@ -1487,7 +1490,7 @@ class TestSalesOrder(FrappeTestCase):
]
}
)
wo_items = so.get_work_order_items()
wo_items = get_work_order_items(so.name)
self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
@@ -1497,6 +1500,8 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
item = make_item(
"_Test Finished Item",
{
@@ -1529,7 +1534,7 @@ class TestSalesOrder(FrappeTestCase):
so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]})
so.submit()
mr_dict = frappe._dict()
items = so.get_work_order_items(1)
items = get_work_order_items(so.name, 1)
mr_dict["items"] = items
mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1

View File

@@ -522,7 +522,7 @@ erpnext.PointOfSale.Controller = class {
const from_selector = field === 'qty' && value === "+1";
if (from_selector)
value = flt(item_row.qty) + flt(value);
value = flt(item_row.stock_qty) + flt(value);
if (item_row_exists) {
if (field === 'qty')

View File

@@ -418,8 +418,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
callback: function(r) {
if(r.message) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
} else {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
}
}
});

View File

@@ -33,6 +33,9 @@ frappe.ui.form.on("Item", {
'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items");
},
'Stock Entry': () => {
open_form(frm, "Stock Entry", "Stock Entry Detail", "items");
},
};
},
@@ -893,6 +896,9 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description;
if (!new_child_doc.qty) {
new_child_doc.qty = 1.0;
}
frappe.run_serially([
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),

View File

@@ -2,7 +2,18 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Item Price", {
onload: function (frm) {
setup(frm) {
frm.set_query("item_code", function() {
return {
filters: {
"disabled": 0,
"has_variants": 0
}
};
});
},
onload(frm) {
// Fetch price list details
frm.add_fetch("price_list", "buying", "buying");
frm.add_fetch("price_list", "selling", "selling");

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe import _, bold
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Cast_
@@ -21,6 +21,7 @@ class ItemPrice(Document):
self.update_price_list_details()
self.update_item_details()
self.check_duplicates()
self.validate_item_template()
def validate_item(self):
if not frappe.db.exists("Item", self.item_code):
@@ -49,6 +50,12 @@ class ItemPrice(Document):
"Item", self.item_code, ["item_name", "description"]
)
def validate_item_template(self):
if frappe.get_cached_value("Item", self.item_code, "has_variants"):
msg = f"Item Price cannot be created for the template item {bold(self.item_code)}"
frappe.throw(_(msg))
def check_duplicates(self):
item_price = frappe.qb.DocType("Item Price")

View File

@@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase):
frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True)
def test_template_item_price(self):
from erpnext.stock.doctype.item.test_item import make_item
item = make_item(
"Test Template Item 1",
{
"has_variants": 1,
"variant_based_on": "Manufacturer",
},
)
doc = frappe.get_doc(
{
"doctype": "Item Price",
"price_list": "_Test Price List",
"item_code": item.name,
"price_list_rate": 100,
}
)
self.assertRaises(frappe.ValidationError, doc.save)
def test_duplicate_item(self):
doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.save)

View File

@@ -55,7 +55,6 @@ class LandedCostVoucher(Document):
self.get_items_from_purchase_receipts()
self.set_applicable_charges_on_item()
self.validate_applicable_charges_for_item()
def check_mandatory(self):
if not self.get("purchase_receipts"):
@@ -115,6 +114,13 @@ class LandedCostVoucher(Document):
total_item_cost += item.get(based_on_field)
for item in self.get("items"):
if not total_item_cost and not item.get(based_on_field):
frappe.throw(
_(
"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'"
)
)
item.applicable_charges = flt(
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
item.precision("applicable_charges"),
@@ -162,6 +168,7 @@ class LandedCostVoucher(Document):
)
def on_submit(self):
self.validate_applicable_charges_for_item()
self.update_landed_cost()
def on_cancel(self):

View File

@@ -175,6 +175,59 @@ class TestLandedCostVoucher(FrappeTestCase):
)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
def test_landed_cost_voucher_for_zero_purchase_rate(self):
"Test impact of LCV on future stock balances."
from erpnext.stock.doctype.item.test_item import make_item
item = make_item("LCV Stock Item", {"is_stock_item": 1})
warehouse = "Stores - _TC"
pr = make_purchase_receipt(
item_code=item.name,
warehouse=warehouse,
qty=10,
rate=0,
posting_date=add_days(frappe.utils.nowdate(), -2),
)
self.assertEqual(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
"stock_value_difference",
),
0,
)
lcv = make_landed_cost_voucher(
company=pr.company,
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=100,
distribute_charges_based_on="Distribute Manually",
do_not_save=True,
)
lcv.get_items_from_purchase_receipts()
lcv.items[0].applicable_charges = 100
lcv.save()
lcv.submit()
self.assertTrue(
frappe.db.exists(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
)
)
self.assertEqual(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
"stock_value_difference",
),
100,
)
def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice(
@@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args):
lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = args.company or "_Test Company"
lcv.distribute_charges_based_on = "Amount"
lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
lcv.set(
"purchase_receipts",

View File

@@ -590,6 +590,9 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.purpose = source.material_request_type
target.from_warehouse = source.set_from_warehouse
target.to_warehouse = source.set_warehouse
if source.job_card:
target.purpose = "Material Transfer for Manufacture"
@@ -725,6 +728,7 @@ def create_pick_list(source_name, target_doc=None):
def make_in_transit_stock_entry(source_name, in_transit_warehouse):
ste_doc = make_stock_entry(source_name)
ste_doc.add_to_transit = 1
ste_doc.to_warehouse = in_transit_warehouse
for row in ste_doc.items:
row.t_warehouse = in_transit_warehouse

View File

@@ -473,7 +473,7 @@ class PurchaseReceipt(BuyingController):
)
divisional_loss = flt(
valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount")
valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount")
)
if divisional_loss:
@@ -1134,13 +1134,25 @@ def get_item_account_wise_additional_cost(purchase_document):
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
"amount"
] += (account.amount * item.get(based_on_field) / total_item_cost)
if total_item_cost > 0:
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["amount"] += (
account.amount * item.get(based_on_field) / total_item_cost
)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
"base_amount"
] += (account.base_amount * item.get(based_on_field) / total_item_cost)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["base_amount"] += (
account.base_amount * item.get(based_on_field) / total_item_cost
)
else:
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["amount"] += item.applicable_charges
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["base_amount"] += item.applicable_charges
return item_account_wise_cost

View File

@@ -4053,7 +4053,7 @@ Server Error,Serverfehler,
Service Level Agreement has been changed to {0}.,Service Level Agreement wurde in {0} geändert.,
Service Level Agreement was reset.,Service Level Agreement wurde zurückgesetzt.,
Service Level Agreement with Entity Type {0} and Entity {1} already exists.,Service Level Agreement mit Entitätstyp {0} und Entität {1} ist bereits vorhanden.,
Set,Menge,
Set Loyalty Program,Treueprogramm eintragen,
Set Meta Tags,Festlegen von Meta-Tags,
Set {0} in company {1},{0} in Firma {1} festlegen,
Setup,Einstellungen,
@@ -4233,10 +4233,8 @@ To date cannot be before From date,Bis-Datum kann nicht vor Von-Datum liegen,
Write Off,Abschreiben,
{0} Created,{0} Erstellt,
Email Id,E-Mail-ID,
No,Kein,
Reference Doctype,Referenz-DocType,
User Id,Benutzeridentifikation,
Yes,Ja,
Actual ,Tatsächlich,
Add to cart,In den Warenkorb legen,
Budget,Budget,
Can't render this file because it is too large.