Merge pull request #34239 from frappe/version-13-hotfix

chore: release v13
This commit is contained in:
ruthra kumar
2023-02-28 18:57:46 +05:30
committed by GitHub
24 changed files with 431 additions and 149 deletions

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 {}."
@@ -652,7 +652,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

@@ -1078,7 +1078,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

@@ -364,6 +364,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)
@@ -373,7 +374,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()
@@ -563,7 +563,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:
@@ -581,7 +581,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
) )
@@ -597,15 +597,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)
@@ -840,24 +837,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

@@ -296,10 +296,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
@@ -519,19 +515,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:
@@ -545,9 +584,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")):
@@ -555,6 +592,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

@@ -817,7 +817,9 @@ def get_leave_balance_on(
allocation = allocation_records.get(leave_type, frappe._dict()) allocation = allocation_records.get(leave_type, frappe._dict())
end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date) cf_expiry = get_allocation_expiry_for_cf_leaves(
employee, leave_type, to_date, allocation.from_date
)
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date) leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
@@ -832,6 +834,7 @@ def get_leave_balance_on(
def get_leave_allocation_records(employee, date, leave_type=None): def get_leave_allocation_records(employee, date, leave_type=None):
"""Returns the total allocated leaves and carry forwarded leaves based on ledger entries""" """Returns the total allocated leaves and carry forwarded leaves based on ledger entries"""
Ledger = frappe.qb.DocType("Leave Ledger Entry") Ledger = frappe.qb.DocType("Leave Ledger Entry")
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
cf_leave_case = ( cf_leave_case = (
frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0) frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0)
@@ -845,6 +848,8 @@ def get_leave_allocation_records(employee, date, leave_type=None):
query = ( query = (
frappe.qb.from_(Ledger) frappe.qb.from_(Ledger)
.inner_join(LeaveAllocation)
.on(Ledger.transaction_name == LeaveAllocation.name)
.select( .select(
sum_cf_leaves, sum_cf_leaves,
sum_new_leaves, sum_new_leaves,
@@ -854,12 +859,21 @@ def get_leave_allocation_records(employee, date, leave_type=None):
) )
.where( .where(
(Ledger.from_date <= date) (Ledger.from_date <= date)
& (Ledger.to_date >= date)
& (Ledger.docstatus == 1) & (Ledger.docstatus == 1)
& (Ledger.transaction_type == "Leave Allocation") & (Ledger.transaction_type == "Leave Allocation")
& (Ledger.employee == employee) & (Ledger.employee == employee)
& (Ledger.is_expired == 0) & (Ledger.is_expired == 0)
& (Ledger.is_lwp == 0) & (Ledger.is_lwp == 0)
& (
# newly allocated leave's end date is same as the leave allocation's to date
((Ledger.is_carry_forward == 0) & (Ledger.to_date >= date))
# carry forwarded leave's end date won't be same as the leave allocation's to date
# it's between the leave allocation's from and to date
| (
(Ledger.is_carry_forward == 1)
& (Ledger.to_date.between(LeaveAllocation.from_date, LeaveAllocation.to_date))
)
)
) )
) )
@@ -925,8 +939,12 @@ def get_remaining_leaves(
# balance for carry forwarded leaves # balance for carry forwarded leaves
if cf_expiry and allocation.unused_leaves: if cf_expiry and allocation.unused_leaves:
cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) if getdate(date) > getdate(cf_expiry):
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry) # carry forwarded leave expiry date passed
cf_leaves = remaining_cf_leaves = 0
else:
cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves) leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves) leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)

View File

@@ -698,8 +698,7 @@ class TestLeaveApplication(unittest.TestCase):
leave_type_name="_Test_CF_leave_expiry", leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1, is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90, expire_carry_forwarded_leaves_after_days=90,
) ).insert()
leave_type.insert()
create_carry_forwarded_allocation(employee, leave_type) create_carry_forwarded_allocation(employee, leave_type)
details = get_leave_balance_on( details = get_leave_balance_on(
@@ -992,17 +991,51 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(leave_allocation, expected) self.assertEqual(leave_allocation, expected)
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company") @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_get_leave_allocation_records(self): def test_leave_details_with_expired_cf_leaves(self):
employee = get_employee() employee = get_employee()
leave_type = create_leave_type( leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry", leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1, is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90, expire_carry_forwarded_leaves_after_days=90,
) ).insert()
leave_type.insert()
leave_alloc = create_carry_forwarded_allocation(employee, leave_type) leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
details = get_leave_allocation_records(employee.name, getdate(), leave_type.name) cf_expiry = frappe.db.get_value(
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
)
# all leaves available before cf leave expiry
leave_details = get_leave_details(employee.name, add_days(cf_expiry, -1))
self.assertEqual(leave_details["leave_allocation"][leave_type.name]["remaining_leaves"], 30.0)
# cf leaves expired
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 1))
expected_data = {
"total_leaves": 30.0,
"expired_leaves": 15.0,
"leaves_taken": 0.0,
"leaves_pending_approval": 0.0,
"remaining_leaves": 15.0,
}
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_get_leave_allocation_records(self):
"""Tests if total leaves allocated before and after carry forwarded leave expiry is same"""
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
).insert()
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
cf_expiry = frappe.db.get_value(
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
)
# test total leaves allocated before cf leave expiry
details = get_leave_allocation_records(employee.name, add_days(cf_expiry, -1), leave_type.name)
expected_data = { expected_data = {
"from_date": getdate(leave_alloc.from_date), "from_date": getdate(leave_alloc.from_date),
"to_date": getdate(leave_alloc.to_date), "to_date": getdate(leave_alloc.to_date),
@@ -1013,6 +1046,11 @@ class TestLeaveApplication(unittest.TestCase):
} }
self.assertEqual(details.get(leave_type.name), expected_data) self.assertEqual(details.get(leave_type.name), expected_data)
# test leaves allocated after carry forwarded leaves expiry, should be same thoroughout allocation period
# cf leaves should show up under expired or taken leaves later
details = get_leave_allocation_records(employee.name, add_days(cf_expiry, 1), leave_type.name)
self.assertEqual(details.get(leave_type.name), expected_data)
def create_carry_forwarded_allocation(employee, leave_type): def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation # initial leave allocation

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

@@ -1,16 +1,61 @@
import frappe import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.india.setup import make_custom_fields
def execute(): def execute():
if frappe.get_all("Company", filters={"country": "India"}): if frappe.get_all("Company", filters={"country": "India"}):
frappe.reload_doc("accounts", "doctype", "POS Invoice") custom_fields = get_non_profit_custom_fields()
frappe.reload_doc("accounts", "doctype", "POS Invoice Item") create_custom_fields(custom_fields, update=True)
make_custom_fields()
if not frappe.db.exists("Party Type", "Donor"): if not frappe.db.exists("Party Type", "Donor"):
frappe.get_doc( frappe.get_doc(
{"doctype": "Party Type", "party_type": "Donor", "account_type": "Receivable"} {"doctype": "Party Type", "party_type": "Donor", "account_type": "Receivable"}
).insert(ignore_permissions=True) ).insert(ignore_permissions=True, ignore_mandatory=True)
def get_non_profit_custom_fields():
return {
"Company": [
{
"fieldname": "non_profit_section",
"label": "Non Profit Settings",
"fieldtype": "Section Break",
"insert_after": "asset_received_but_not_billed",
"collapsible": 1,
},
{
"fieldname": "company_80g_number",
"label": "80G Number",
"fieldtype": "Data",
"insert_after": "non_profit_section",
},
{
"fieldname": "with_effect_from",
"label": "80G With Effect From",
"fieldtype": "Date",
"insert_after": "company_80g_number",
},
{
"fieldname": "pan_details",
"label": "PAN Number",
"fieldtype": "Data",
"insert_after": "with_effect_from",
},
],
"Member": [
{
"fieldname": "pan_number",
"label": "PAN Details",
"fieldtype": "Data",
"insert_after": "email_id",
},
],
"Donor": [
{
"fieldname": "pan_number",
"label": "PAN Details",
"fieldtype": "Data",
"insert_after": "email",
},
],
}

View File

@@ -124,8 +124,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
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

@@ -280,9 +280,12 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
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({
@@ -292,14 +295,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
}); });
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',
@@ -400,9 +396,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
make_raw_material_request: function() { make_raw_material_request: function() {
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) {
@@ -421,6 +417,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
}, },
make_raw_material_request_dialog: function(r) { make_raw_material_request_dialog: function(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 six import string_types from six import string_types
@@ -481,51 +482,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)
@@ -1399,3 +1355,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

@@ -1211,6 +1211,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(
**{ **{
@@ -1224,7 +1226,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"),
@@ -1415,6 +1417,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",
@@ -1454,7 +1457,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)
@@ -1464,6 +1467,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",
{ {
@@ -1496,7 +1501,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 = erpnext.TransactionController.extend({
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");
},
}; };
}, },
@@ -848,6 +851,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.utils import getdate from frappe.utils import getdate
@@ -19,6 +19,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):
@@ -47,6 +48,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):
conditions = ( conditions = (
"""where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s"""

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

@@ -594,6 +594,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"

View File

@@ -1064,13 +1064,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

@@ -4047,7 +4047,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,
@@ -4227,10 +4227,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.