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

View File

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

View File

@@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
bold_item_name = frappe.bold(item.item_name) bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold( 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) 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), ).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"), 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( frappe.throw(
_( _(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
), ),
title=_("Item Unavailable"), 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( frappe.throw(
_( _(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." "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) item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty 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( if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item" "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(); dialog.hide();
return frappe.call({ return frappe.call({
method: "frappe.client.set_value", method: "frappe.client.set_value",

View File

@@ -395,6 +395,7 @@ def get_column_names():
class GrossProfitGenerator(object): class GrossProfitGenerator(object):
def __init__(self, filters=None): def __init__(self, filters=None):
self.sle = {}
self.data = [] self.data = []
self.average_buying_rate = {} self.average_buying_rate = {}
self.filters = frappe._dict(filters) self.filters = frappe._dict(filters)
@@ -404,7 +405,6 @@ class GrossProfitGenerator(object):
if filters.group_by == "Invoice": if filters.group_by == "Invoice":
self.group_items_by_invoice() self.group_items_by_invoice()
self.load_stock_ledger_entries()
self.load_product_bundle() self.load_product_bundle()
self.load_non_stock_items() self.load_non_stock_items()
self.get_returned_invoice_items() self.get_returned_invoice_items()
@@ -633,7 +633,7 @@ class GrossProfitGenerator(object):
return flt(row.qty) * item_rate return flt(row.qty) * item_rate
else: 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: if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent parenttype, parent = row.parenttype, row.parent
if row.dn_detail: if row.dn_detail:
@@ -651,7 +651,7 @@ class GrossProfitGenerator(object):
dn["item_row"], dn["item_row"],
dn["warehouse"], 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( return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code 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): def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item") delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = ( query = (
frappe.qb.from_(delivery_note) frappe.qb.from_(delivery_note_item)
.inner_join(delivery_note_item)
.on(delivery_note.name == delivery_note_item.parent)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) .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.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order) .where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail) .where(delivery_note_item.so_detail == so_detail)
@@ -940,24 +937,36 @@ class GrossProfitGenerator(object):
"Item", item_code, ["item_name", "description", "item_group", "brand"] "Item", item_code, ["item_name", "description", "item_group", "brand"]
) )
def load_stock_ledger_entries(self): def get_stock_ledger_entries(self, item_code, warehouse):
res = frappe.db.sql( if item_code and warehouse:
"""select item_code, voucher_type, voucher_no, if (item_code, warehouse) not in self.sle:
voucher_detail_no, stock_value, warehouse, actual_qty as qty sle = qb.DocType("Stock Ledger Entry")
from `tabStock Ledger Entry` res = (
where company=%(company)s and is_cancelled = 0 qb.from_(sle)
order by .select(
item_code desc, warehouse desc, posting_date desc, sle.item_code,
posting_time desc, creation desc""", sle.voucher_type,
self.filters, sle.voucher_no,
as_dict=True, sle.voucher_detail_no,
) sle.stock_value,
self.sle = {} sle.warehouse,
for r in res: sle.actual_qty.as_("qty"),
if (r.item_code, r.warehouse) not in self.sle: )
self.sle[(r.item_code, r.warehouse)] = [] .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): def load_product_bundle(self):
self.product_bundles = {} 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)); // 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) { make_schedules_editable: function(frm) {
if (frm.doc.finance_books) { if (frm.doc.finance_books) {
var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 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) { 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) { erpnext.asset.set_accumulated_depreciation = function(frm, finance_book_id) {
if(frm.doc.depreciation_method != "Manual") return; 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); var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) { $.each(frm.doc.schedules || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount); if (row.finance_book_id === finance_book_id) {
frappe.model.set_value(row.doctype, row.name, accumulated_depreciation += flt(row.depreciation_amount);
"accumulated_depreciation_amount", accumulated_depreciation); 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: if self.calculate_depreciation:
self.value_after_depreciation = 0 self.value_after_depreciation = 0
self.set_depreciation_rate() self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_disposal) if self.should_prepare_depreciation_schedule():
self.set_accumulated_depreciation(date_of_disposal, date_of_return) self.make_depreciation_schedule(date_of_disposal)
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
else: else:
self.finance_books = [] self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation 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): def validate_item(self):
item = frappe.get_cached_value( item = frappe.get_cached_value(
"Item", self.item_code, ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1 "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): 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( if not self.get("schedules"):
"schedules"
):
self.schedules = [] self.schedules = []
if not self.available_for_use_date: if not self.available_for_use_date:
@@ -555,9 +594,7 @@ class Asset(AccountsController):
def set_accumulated_depreciation( def set_accumulated_depreciation(
self, date_of_disposal=None, date_of_return=None, ignore_booked_entry=False self, date_of_disposal=None, date_of_return=None, ignore_booked_entry=False
): ):
straight_line_idx = [ straight_line_idx = []
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
]
finance_books = [] finance_books = []
for i, d in enumerate(self.get("schedules")): for i, d in enumerate(self.get("schedules")):
@@ -565,6 +602,12 @@ class Asset(AccountsController):
continue continue
if int(d.finance_book_id) not in finance_books: 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) accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt( value_after_depreciation = flt(
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation 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", fieldname:"from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "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 reqd: 1
}, },
{ {
fieldname:"to_date", fieldname:"to_date",
label: __("To Date"), label: __("To Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.get_today(),
reqd: 1 reqd: 1
}, },
] ]

View File

@@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
fieldname:"from_date", fieldname:"from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "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 reqd: 1
}, },
{ {
fieldname:"to_date", fieldname:"to_date",
label: __("To Date"), label: __("To Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.get_today(),
reqd: 1 reqd: 1
}, },
] ]

View File

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

View File

@@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = {
], ],
"formatter": function(value, row, column, data, default_formatter) { "formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);
if (column.id == "item") { 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>`; value = `<a style='color:green' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else { } else {
value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`; 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)); item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
} }
else { else {
let qty = item.qty || 1; // allow for '0' qty on Credit/Debit notes
qty = me.frm.doc.is_return ? -1 * qty : qty; let qty = item.qty || -1
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); 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() { make_work_order() {
var me = this; var me = this;
this.frm.call({ me.frm.call({
doc: this.frm.doc, method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
method: 'get_work_order_items', args: {
sales_order: this.frm.docname,
},
freeze: true,
callback: function(r) { callback: function(r) {
if(!r.message) { if(!r.message) {
frappe.msgprint({ frappe.msgprint({
@@ -321,14 +324,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}); });
return; return;
} }
else if(!r.message) { else {
frappe.msgprint({
title: __('Work Order not created'),
message: __('Work Order already created for all items with BOM'),
indicator: 'orange'
});
return;
} else {
const fields = [{ const fields = [{
label: 'Items', label: 'Items',
fieldtype: 'Table', fieldtype: 'Table',
@@ -429,9 +425,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_raw_material_request() { make_raw_material_request() {
var me = this; var me = this;
this.frm.call({ this.frm.call({
doc: this.frm.doc, method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
method: 'get_work_order_items',
args: { args: {
sales_order: this.frm.docname,
for_raw_material_request: 1 for_raw_material_request: 1
}, },
callback: function(r) { callback: function(r) {
@@ -450,6 +446,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} }
make_raw_material_request_dialog(r) { make_raw_material_request_dialog(r) {
var me = this;
var fields = [ var fields = [
{fieldtype:'Check', fieldname:'include_exploded_items', {fieldtype:'Check', fieldname:'include_exploded_items',
label: __('Include Exploded Items')}, label: __('Include Exploded Items')},

View File

@@ -6,11 +6,12 @@ import json
import frappe import frappe
import frappe.utils import frappe.utils
from frappe import _ from frappe import _, qb
from frappe.contacts.doctype.address.address import get_company_address from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values 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 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 ( from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
@@ -414,51 +415,6 @@ class SalesOrder(SellingController):
self.indicator_color = "green" self.indicator_color = "green"
self.indicator_title = _("Paid") 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 on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): 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) 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 return
frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty) 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")) self.assertTrue(si.get("payment_schedule"))
def test_make_work_order(self): def test_make_work_order(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
# Make a new Sales Order # Make a new Sales Order
so = make_sales_order( so = make_sales_order(
**{ **{
@@ -1230,7 +1232,7 @@ class TestSalesOrder(FrappeTestCase):
# Raise Work Orders # Raise Work Orders
po_items = [] po_items = []
so_item_name = {} so_item_name = {}
for item in so.get_work_order_items(): for item in get_work_order_items(so.name):
po_items.append( po_items.append(
{ {
"warehouse": item.get("warehouse"), "warehouse": item.get("warehouse"),
@@ -1448,6 +1450,7 @@ class TestSalesOrder(FrappeTestCase):
from erpnext.controllers.item_variant import create_variant from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom 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 make_item( # template item
"Test-WO-Tshirt", "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("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name) 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) self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self): def test_request_for_raw_materials(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
item = make_item( item = make_item(
"_Test Finished 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 = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]})
so.submit() so.submit()
mr_dict = frappe._dict() 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["items"] = items
mr_dict["include_exploded_items"] = 0 mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1 mr_dict["ignore_existing_ordered_qty"] = 1

View File

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

View File

@@ -418,8 +418,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
callback: function(r) { callback: function(r) {
if(r.message) { if(r.message) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', 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': () => { 'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items"); 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.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom; new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description; new_child_doc.description = frm.doc.description;
if (!new_child_doc.qty) {
new_child_doc.qty = 1.0;
}
frappe.run_serially([ frappe.run_serially([
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc), () => 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 // License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Item Price", { 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 // Fetch price list details
frm.add_fetch("price_list", "buying", "buying"); frm.add_fetch("price_list", "buying", "buying");
frm.add_fetch("price_list", "selling", "selling"); frm.add_fetch("price_list", "selling", "selling");

View File

@@ -3,7 +3,7 @@
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Criterion from frappe.query_builder import Criterion
from frappe.query_builder.functions import Cast_ from frappe.query_builder.functions import Cast_
@@ -21,6 +21,7 @@ class ItemPrice(Document):
self.update_price_list_details() self.update_price_list_details()
self.update_item_details() self.update_item_details()
self.check_duplicates() self.check_duplicates()
self.validate_item_template()
def validate_item(self): def validate_item(self):
if not frappe.db.exists("Item", self.item_code): if not frappe.db.exists("Item", self.item_code):
@@ -49,6 +50,12 @@ class ItemPrice(Document):
"Item", self.item_code, ["item_name", "description"] "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): def check_duplicates(self):
item_price = frappe.qb.DocType("Item Price") item_price = frappe.qb.DocType("Item Price")

View File

@@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase):
frappe.db.sql("delete from `tabItem Price`") frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True) 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): def test_duplicate_item(self):
doc = frappe.copy_doc(test_records[0]) doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.save) self.assertRaises(ItemPriceDuplicateItem, doc.save)

View File

@@ -55,7 +55,6 @@ class LandedCostVoucher(Document):
self.get_items_from_purchase_receipts() self.get_items_from_purchase_receipts()
self.set_applicable_charges_on_item() self.set_applicable_charges_on_item()
self.validate_applicable_charges_for_item()
def check_mandatory(self): def check_mandatory(self):
if not self.get("purchase_receipts"): if not self.get("purchase_receipts"):
@@ -115,6 +114,13 @@ class LandedCostVoucher(Document):
total_item_cost += item.get(based_on_field) total_item_cost += item.get(based_on_field)
for item in self.get("items"): 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( item.applicable_charges = flt(
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
item.precision("applicable_charges"), item.precision("applicable_charges"),
@@ -162,6 +168,7 @@ class LandedCostVoucher(Document):
) )
def on_submit(self): def on_submit(self):
self.validate_applicable_charges_for_item()
self.update_landed_cost() self.update_landed_cost()
def on_cancel(self): 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) 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): def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice( pi = make_purchase_invoice(
@@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args):
lcv = frappe.new_doc("Landed Cost Voucher") lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = args.company or "_Test Company" 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( lcv.set(
"purchase_receipts", "purchase_receipts",

View File

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

View File

@@ -473,7 +473,7 @@ class PurchaseReceipt(BuyingController):
) )
divisional_loss = flt( 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: 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} account.expense_account, {"amount": 0.0, "base_amount": 0.0}
) )
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ if total_item_cost > 0:
"amount" item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
] += (account.amount * item.get(based_on_field) / total_item_cost) 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][ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
"base_amount" account.expense_account
] += (account.base_amount * item.get(based_on_field) / total_item_cost) ]["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 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 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 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., 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 Meta Tags,Festlegen von Meta-Tags,
Set {0} in company {1},{0} in Firma {1} festlegen, Set {0} in company {1},{0} in Firma {1} festlegen,
Setup,Einstellungen, Setup,Einstellungen,
@@ -4233,10 +4233,8 @@ To date cannot be before From date,Bis-Datum kann nicht vor Von-Datum liegen,
Write Off,Abschreiben, Write Off,Abschreiben,
{0} Created,{0} Erstellt, {0} Created,{0} Erstellt,
Email Id,E-Mail-ID, Email Id,E-Mail-ID,
No,Kein,
Reference Doctype,Referenz-DocType, Reference Doctype,Referenz-DocType,
User Id,Benutzeridentifikation, User Id,Benutzeridentifikation,
Yes,Ja,
Actual ,Tatsächlich, Actual ,Tatsächlich,
Add to cart,In den Warenkorb legen, Add to cart,In den Warenkorb legen,
Budget,Budget, Budget,Budget,
Can't render this file because it is too large.