Compare commits
53 Commits
v14.85.9
...
version-14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a06d705d75 | ||
|
|
9e3e3182f2 | ||
|
|
1393100a62 | ||
|
|
f07594b863 | ||
|
|
25a382ec24 | ||
|
|
d7c47e8ea5 | ||
|
|
2627fb10ce | ||
|
|
0790db79fd | ||
|
|
0dad2acfc8 | ||
|
|
193042ef07 | ||
|
|
8208a9ec27 | ||
|
|
fbfc6a6f62 | ||
|
|
e9f9eaa2d0 | ||
|
|
08d40ddb9b | ||
|
|
d60ab92082 | ||
|
|
85a0581145 | ||
|
|
2cac05e56c | ||
|
|
10e4e610db | ||
|
|
9ab80cfd6c | ||
|
|
8f2b83d434 | ||
|
|
7f0ebb37da | ||
|
|
1d42c4a305 | ||
|
|
186173a21e | ||
|
|
96c937bf6a | ||
|
|
52aafc9410 | ||
|
|
3232310c0f | ||
|
|
6397c366fd | ||
|
|
f5ef376486 | ||
|
|
c9eeca22f5 | ||
|
|
b145638eca | ||
|
|
b46fdc2645 | ||
|
|
df779bb7dd | ||
|
|
20f701ff0b | ||
|
|
ca4858318e | ||
|
|
b664781fae | ||
|
|
69463b4798 | ||
|
|
caf0f2cd19 | ||
|
|
1cfb002de8 | ||
|
|
3b686827e9 | ||
|
|
d37b12f385 | ||
|
|
43aeff38aa | ||
|
|
ad13dcd695 | ||
|
|
792f3afa1b | ||
|
|
43dda4a5d2 | ||
|
|
274ce10329 | ||
|
|
caf145e3ca | ||
|
|
05911ad563 | ||
|
|
de937a6bad | ||
|
|
70e6ea6b3f | ||
|
|
70bcfb4748 | ||
|
|
1f1cb338fe | ||
|
|
d5d4b3a7f3 | ||
|
|
5d9be7366b |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.85.9"
|
||||
__version__ = "14.86.4"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -3287,6 +3287,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
"""
|
||||
Test a case where duplicating the item with qty = 1 in the invoice
|
||||
@@ -3294,24 +3295,23 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"""
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
|
||||
|
||||
dn = create_delivery_note()
|
||||
dn.submit()
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
# make a copy of first item and add it to invoice
|
||||
item_copy = frappe.copy_doc(si.items[0])
|
||||
si.save()
|
||||
|
||||
si.items = [] # Clear existing items
|
||||
si.append("items", item_copy)
|
||||
si.save()
|
||||
|
||||
si.append("items", item_copy)
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.submit()
|
||||
si.save()
|
||||
|
||||
self.assertTrue("cannot overbill" in str(err.exception).lower())
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
|
||||
dn.cancel()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
|
||||
@@ -622,6 +622,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
conditions.append(ple.party.isin(parties))
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
conditions.append(ple.company == inv.company)
|
||||
conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date])
|
||||
|
||||
advance_amt = (
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||
|
||||
@@ -243,17 +243,18 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
|
||||
vouchers = []
|
||||
|
||||
# create advance payment
|
||||
pe = create_payment_entry(
|
||||
pe1 = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
|
||||
)
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_to = "Cash - _TC"
|
||||
pe.submit()
|
||||
vouchers.append(pe)
|
||||
pe1.paid_from = "Debtors - _TC"
|
||||
pe1.paid_to = "Cash - _TC"
|
||||
pe1.submit()
|
||||
vouchers.append(pe1)
|
||||
|
||||
# create invoice
|
||||
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
|
||||
@@ -275,6 +276,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
# make another invoice
|
||||
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
|
||||
# TDS should be calculated
|
||||
|
||||
# this payment should not be considered for TCS calculation as it is outside of fiscal year
|
||||
pe2 = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=10000
|
||||
)
|
||||
pe2.paid_from = "Debtors - _TC"
|
||||
pe2.paid_to = "Cash - _TC"
|
||||
pe2.posting_date = add_days(fiscal_year[1], -10)
|
||||
pe2.submit()
|
||||
vouchers.append(pe2)
|
||||
|
||||
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
|
||||
si2.submit()
|
||||
vouchers.append(si2)
|
||||
|
||||
@@ -118,7 +118,8 @@ class ReceivablePayableReport:
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True)
|
||||
query, param = self.ple_query.walk()
|
||||
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
@@ -131,8 +132,9 @@ class ReceivablePayableReport:
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
query, param = self.ple_query.walk()
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True):
|
||||
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
args: {
|
||||
item_code: item.item_code,
|
||||
warehouse: cstr(item.warehouse),
|
||||
qty: flt(item.stock_qty),
|
||||
qty: -1 * flt(item.stock_qty),
|
||||
serial_no: item.serial_no,
|
||||
posting_date: me.frm.doc.posting_date,
|
||||
posting_time: me.frm.doc.posting_time,
|
||||
|
||||
@@ -20,6 +20,9 @@ def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
|
||||
|
||||
for d in doc.get("items"):
|
||||
if d.get("is_free_item"):
|
||||
continue
|
||||
|
||||
# get last purchase details
|
||||
last_purchase_details = get_last_purchase_details(d.item_code, doc.name)
|
||||
|
||||
|
||||
@@ -1742,69 +1742,50 @@ class AccountsController(TransactionBase):
|
||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
|
||||
item_allowance = {}
|
||||
global_qty_allowance, global_amount_allowance = None, None
|
||||
ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
|
||||
|
||||
role_allowed_to_over_bill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
user_roles = frappe.get_roles()
|
||||
if not ref_wise_billed_amount:
|
||||
return
|
||||
|
||||
total_overbilled_amt = 0.0
|
||||
overbilled_items = []
|
||||
precision = self.precision(based_on, "items")
|
||||
precision_allowance = 1 / (10**precision)
|
||||
|
||||
reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
role_allowed_to_overbill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
|
||||
|
||||
for item in self.get("items"):
|
||||
if not item.get(item_ref_dn):
|
||||
continue
|
||||
for row in ref_wise_billed_amount.values():
|
||||
total_billed_amt = row.billed_amt
|
||||
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
|
||||
|
||||
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||
based_on_amt = flt(item.get(based_on))
|
||||
|
||||
if not ref_amt:
|
||||
if based_on_amt: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
|
||||
|
||||
total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
|
||||
|
||||
allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
|
||||
item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
|
||||
)
|
||||
|
||||
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
|
||||
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
|
||||
|
||||
if total_billed_amt < 0 and max_allowed_amt < 0:
|
||||
# while making debit note against purchase return entry(purchase receipt) getting overbill error
|
||||
total_billed_amt = abs(total_billed_amt)
|
||||
max_allowed_amt = abs(max_allowed_amt)
|
||||
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
|
||||
|
||||
overbill_amt = total_billed_amt - max_allowed_amt
|
||||
row["max_allowed_amt"] = max_allowed_amt
|
||||
total_overbilled_amt += overbill_amt
|
||||
|
||||
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(
|
||||
if overbill_amt > precision_allowance and not is_overbilling_allowed:
|
||||
if self.doctype != "Purchase Invoice" or not cint(
|
||||
frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
overbilled_items.append(row)
|
||||
|
||||
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
|
||||
if overbilled_items:
|
||||
self.throw_overbill_exception(overbilled_items, precision)
|
||||
|
||||
if is_overbilling_allowed and total_overbilled_amt > 0.1:
|
||||
frappe.msgprint(
|
||||
_("Overbilling of {} ignored because you have {} role.").format(
|
||||
total_overbilled_amt, role_allowed_to_over_bill
|
||||
total_overbilled_amt, role_allowed_to_overbill
|
||||
),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
@@ -1820,55 +1801,88 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
|
||||
def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
|
||||
"""
|
||||
Returns Sum of Amount of
|
||||
Sales/Purchase Invoice Items
|
||||
that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
|
||||
that are submitted OR not submitted but are under current invoice
|
||||
"""
|
||||
reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]
|
||||
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
if not reference_names:
|
||||
return
|
||||
|
||||
item_doctype = frappe.qb.DocType(item.doctype)
|
||||
ref_wise_billed_amount = {}
|
||||
precision = self.precision(based_on, "items")
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
|
||||
|
||||
for item in self.items:
|
||||
key = item.get(item_ref_dn)
|
||||
if not key:
|
||||
continue
|
||||
|
||||
ref_amt = flt(reference_details.get(key), precision)
|
||||
current_amount = flt(item.get(based_on), precision)
|
||||
|
||||
if not ref_amt:
|
||||
if current_amount: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
ref_wise_billed_amount.setdefault(
|
||||
key,
|
||||
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
|
||||
)
|
||||
|
||||
ref_wise_billed_amount[key]["rows"].append(item.idx)
|
||||
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
|
||||
ref_wise_billed_amount[key]["billed_amt"] += current_amount
|
||||
if key in already_billed:
|
||||
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
|
||||
|
||||
return ref_wise_billed_amount
|
||||
|
||||
def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
|
||||
item_doctype = frappe.qb.DocType(self.items[0].doctype)
|
||||
based_on_field = frappe.qb.Field(based_on)
|
||||
join_field = frappe.qb.Field(item_ref_dn)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(Sum(based_on_field))
|
||||
.where(join_field == item.get(item_ref_dn))
|
||||
.where(
|
||||
Criterion.any(
|
||||
[ # select all items from other invoices OR current invoices
|
||||
Criterion.all(
|
||||
[ # for selecting items from other invoices
|
||||
item_doctype.docstatus == 1,
|
||||
item_doctype.parent != self.name,
|
||||
]
|
||||
),
|
||||
Criterion.all(
|
||||
[ # for selecting items from current invoice, that are linked to same reference
|
||||
item_doctype.docstatus == 0,
|
||||
item_doctype.parent == self.name,
|
||||
item_doctype.name != item.name,
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
).run()
|
||||
|
||||
return result[0][0] if result else 0
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings"
|
||||
).format(item.item_code, item.idx, max_allowed_amt)
|
||||
return frappe._dict(
|
||||
(
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(join_field, Sum(based_on_field))
|
||||
.where(join_field.isin(reference_names))
|
||||
.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
|
||||
.groupby(join_field)
|
||||
).run()
|
||||
)
|
||||
|
||||
def throw_overbill_exception(self, overbilled_items, precision):
|
||||
message = (
|
||||
_("<p>Cannot overbill for the following Items:</p>")
|
||||
+ "<ul>"
|
||||
+ "".join(
|
||||
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
|
||||
frappe.bold(item.item_code),
|
||||
", ".join(str(x) for x in item.rows),
|
||||
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
|
||||
)
|
||||
for item in overbilled_items
|
||||
)
|
||||
+ "</ul>"
|
||||
)
|
||||
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
|
||||
|
||||
frappe.throw(_(message))
|
||||
|
||||
def get_company_default(self, fieldname, ignore_validation=False):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
|
||||
|
||||
@@ -29,4 +29,10 @@ frappe.ui.form.on("Contract", {
|
||||
});
|
||||
}
|
||||
},
|
||||
party_name: function (frm) {
|
||||
let field = frm.doc.party_type.toLowerCase() + "_name";
|
||||
frappe.db.get_value(frm.doc.party_type, frm.doc.party_name, field, (r) => {
|
||||
frm.set_value("party_full_name", r[field]);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"party_user",
|
||||
"status",
|
||||
"fulfilment_status",
|
||||
"party_full_name",
|
||||
"sb_terms",
|
||||
"start_date",
|
||||
"cb_date",
|
||||
@@ -244,11 +245,18 @@
|
||||
"fieldname": "authorised_by_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Authorised By"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_full_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Full Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-07 11:15:58.385521",
|
||||
"modified": "2025-05-23 13:54:03.346537",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Contract",
|
||||
@@ -315,9 +323,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,17 @@ class Contract(Document):
|
||||
self.name = _(name)
|
||||
|
||||
def validate(self):
|
||||
self.set_missing_values()
|
||||
self.validate_dates()
|
||||
self.update_contract_status()
|
||||
self.update_fulfilment_status()
|
||||
|
||||
def set_missing_values(self):
|
||||
if not self.party_full_name:
|
||||
field = self.party_type.lower() + "_name"
|
||||
if res := frappe.db.get_value(self.party_type, self.party_name, field):
|
||||
self.party_full_name = res
|
||||
|
||||
def before_submit(self):
|
||||
self.signed_by_company = frappe.session.user
|
||||
|
||||
|
||||
@@ -1048,6 +1048,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus < 2)
|
||||
@@ -1115,6 +1116,7 @@ def get_subitems(
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bom.name == bom_no)
|
||||
@@ -1228,6 +1230,7 @@ def get_material_request_items(
|
||||
"sales_order": sales_order,
|
||||
"description": row.get("description"),
|
||||
"uom": row.get("purchase_uom") or row.get("stock_uom"),
|
||||
"main_bom_item": row.get("main_bom_item"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1757,6 +1760,7 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus == 1)
|
||||
|
||||
@@ -1322,20 +1322,20 @@ def stop_unstop(work_order, status):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def query_sales_order(production_item):
|
||||
out = frappe.db.sql_list(
|
||||
"""
|
||||
select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item
|
||||
where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1
|
||||
union
|
||||
select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item
|
||||
where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1
|
||||
""",
|
||||
(production_item, production_item),
|
||||
def query_sales_order(production_item: str) -> list[str]:
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
filters=[
|
||||
["Sales Order", "docstatus", "=", 1],
|
||||
],
|
||||
or_filters=[
|
||||
["Sales Order Item", "item_code", "=", production_item],
|
||||
["Packed Item", "item_code", "=", production_item],
|
||||
],
|
||||
pluck="name",
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_job_card(work_order, operations):
|
||||
|
||||
@@ -23,6 +23,7 @@ def get_columns():
|
||||
"""return columns"""
|
||||
columns = [
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + "::240",
|
||||
_("Description") + "::300",
|
||||
_("BOM Qty") + ":Float:160",
|
||||
_("BOM UoM") + "::160",
|
||||
@@ -73,11 +74,12 @@ def get_bom_stock(filters):
|
||||
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||
.select(
|
||||
BOM_ITEM.item_code,
|
||||
BOM_ITEM.item_name,
|
||||
BOM_ITEM.description,
|
||||
BOM_ITEM.stock_qty,
|
||||
BOM_ITEM.stock_uom,
|
||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||
Sum(BIN.actual_qty).as_("actual_qty"),
|
||||
BIN.actual_qty.as_("actual_qty"),
|
||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||
)
|
||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||
|
||||
@@ -94,6 +94,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
|
||||
expected_data.append(
|
||||
[
|
||||
item.item_code,
|
||||
item.item_name,
|
||||
item.description,
|
||||
item.stock_qty,
|
||||
item.stock_uom,
|
||||
|
||||
@@ -375,3 +375,5 @@ erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||
erpnext.patches.v14_0.rename_group_by_to_categorize_by
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
|
||||
erpnext.patches.v14_0.set_update_price_list_based_on
|
||||
erpnext.patches.v14_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||
erpnext.patches.v14_0.update_full_name_in_contract
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
custom_reports = frappe.get_all(
|
||||
"Report",
|
||||
filters={
|
||||
"report_type": "Custom Report",
|
||||
"reference_report": ["in", ["General Ledger", "Supplier Quotation Comparison"]],
|
||||
},
|
||||
fields=["name", "json"],
|
||||
)
|
||||
|
||||
for report in custom_reports:
|
||||
report_json = json.loads(report.json)
|
||||
|
||||
if "filters" in report_json and "group_by" in report_json["filters"]:
|
||||
report_json["filters"]["categorize_by"] = (
|
||||
report_json["filters"].pop("group_by").replace("Group", "Categorize")
|
||||
)
|
||||
|
||||
frappe.db.set_value("Report", report.name, "json", json.dumps(report_json))
|
||||
15
erpnext/patches/v14_0/update_full_name_in_contract.py
Normal file
15
erpnext/patches/v14_0/update_full_name_in_contract.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
|
||||
|
||||
def execute():
|
||||
con = qb.DocType("Contract")
|
||||
for c in (
|
||||
qb.from_(con)
|
||||
.select(con.name, con.party_type, con.party_name)
|
||||
.where(con.party_full_name.isnull())
|
||||
.run(as_dict=True)
|
||||
):
|
||||
field = c.party_type.lower() + "_name"
|
||||
if res := frappe.db.get_value(c.party_type, c.party_name, field):
|
||||
frappe.db.set_value("Contract", c.name, "party_full_name", res)
|
||||
@@ -26,7 +26,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
|
||||
}
|
||||
|
||||
if (item.discount_amount) {
|
||||
if (item.discount_amount > 0) {
|
||||
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
|
||||
item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin);
|
||||
}
|
||||
|
||||
@@ -1266,6 +1266,11 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
||||
|
||||
items.update({"company": company, "sales_order": sales_order})
|
||||
|
||||
item_wh = {}
|
||||
for item in items.get("items"):
|
||||
if item.get("warehouse"):
|
||||
item_wh[item.get("item_code")] = item.get("warehouse")
|
||||
|
||||
raw_materials = get_items_for_material_requests(items)
|
||||
if not raw_materials:
|
||||
frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
|
||||
@@ -1290,7 +1295,7 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
||||
"item_code": item.get("item_code"),
|
||||
"qty": item.get("quantity"),
|
||||
"schedule_date": schedule_date,
|
||||
"warehouse": item.get("warehouse"),
|
||||
"warehouse": item_wh.get(item.get("main_bom_item")) or item.get("warehouse"),
|
||||
"sales_order": sales_order,
|
||||
"project": project,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_guest_to_view": 1,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:item_code",
|
||||
@@ -897,10 +896,9 @@
|
||||
"icon": "fa fa-tag",
|
||||
"idx": 2,
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-01-08 18:09:30.225085",
|
||||
"modified": "2025-02-03 23:43:57.253667",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -497,26 +497,23 @@ erpnext.buying.MaterialRequestController = class MaterialRequestController exten
|
||||
}
|
||||
|
||||
onload(doc, cdt, cdn) {
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
this.frm.set_query("item_code", "items", function () {
|
||||
let filters = { is_stock_item: 1 };
|
||||
|
||||
if (doc.material_request_type == "Customer Provided") {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters:{
|
||||
'customer': me.frm.doc.customer,
|
||||
'is_stock_item':1
|
||||
}
|
||||
}
|
||||
} else if (doc.material_request_type == "Purchase") {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {'is_purchase_item': 1}
|
||||
}
|
||||
} else {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {'is_stock_item': 1}
|
||||
}
|
||||
filters.customer = doc.customer;
|
||||
} else if (
|
||||
doc.material_request_type == "Purchase"
|
||||
) {
|
||||
filters = { is_purchase_item: 1 };
|
||||
} else if (doc.material_request_type == "Manufacture") {
|
||||
filters.include_item_in_manufacturing = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2761,6 +2761,63 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "To Bill")
|
||||
|
||||
def test_serial_no_exists_in_future(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item_doc = make_item(
|
||||
"Test Serial No Item Exists in Future",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-SBNS-.#####",
|
||||
},
|
||||
)
|
||||
|
||||
source_warehouse = "_Test Warehouse - _TC"
|
||||
target_warehouse = "_Test Warehouse 1 - _TC"
|
||||
if not frappe.db.exists("Warehouse", target_warehouse):
|
||||
create_warehouse("_Test Warehouse 1")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
serial_no="SN-SBNS-00001",
|
||||
posting_date=add_days(today(), -2),
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_doc.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
to_warehouse=target_warehouse,
|
||||
serial_no="SN-SBNS-00002",
|
||||
posting_date=add_days(today(), -1),
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_doc.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
from_warehouse=source_warehouse,
|
||||
to_warehouse=target_warehouse,
|
||||
serial_no="SN-SBNS-00001",
|
||||
posting_date=today(),
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_doc.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
from_warehouse=target_warehouse,
|
||||
serial_no="SN-SBNS-00001",
|
||||
posting_date=add_days(today(), -1),
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, se.submit)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -8,7 +8,18 @@ import frappe
|
||||
from frappe import ValidationError, _
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
get_datetime,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
now,
|
||||
nowdate,
|
||||
safe_json_loads,
|
||||
)
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.get_item_details import get_reserved_qty_for_so
|
||||
@@ -178,13 +189,13 @@ class SerialNo(StockController):
|
||||
entries = {}
|
||||
sle_dict = self.get_stock_ledger_entries(serial_no)
|
||||
if sle_dict:
|
||||
last_sle = sle_dict.get("last_sle") or {}
|
||||
entries["last_sle"] = last_sle
|
||||
|
||||
if sle_dict.get("incoming", []):
|
||||
entries["purchase_sle"] = sle_dict["incoming"][-1]
|
||||
|
||||
if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0:
|
||||
entries["last_sle"] = sle_dict["incoming"][0]
|
||||
else:
|
||||
entries["last_sle"] = sle_dict["outgoing"][0]
|
||||
if last_sle.get("actual_qty") < 0 and sle_dict.get("outgoing", []):
|
||||
entries["delivery_sle"] = sle_dict["outgoing"][0]
|
||||
|
||||
return entries
|
||||
@@ -197,7 +208,7 @@ class SerialNo(StockController):
|
||||
for sle in frappe.db.sql(
|
||||
"""
|
||||
SELECT voucher_type, voucher_no,
|
||||
posting_date, posting_time, incoming_rate, actual_qty, serial_no
|
||||
posting_date, posting_time, incoming_rate, actual_qty, serial_no, posting_datetime
|
||||
FROM
|
||||
`tabStock Ledger Entry`
|
||||
WHERE
|
||||
@@ -221,6 +232,9 @@ class SerialNo(StockController):
|
||||
as_dict=1,
|
||||
):
|
||||
if serial_no.upper() in get_serial_nos(sle.serial_no):
|
||||
if "last_sle" not in sle_dict:
|
||||
sle_dict["last_sle"] = sle
|
||||
|
||||
if cint(sle.actual_qty) > 0:
|
||||
sle_dict.setdefault("incoming", []).append(sle)
|
||||
else:
|
||||
@@ -248,8 +262,23 @@ class SerialNo(StockController):
|
||||
_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name)
|
||||
)
|
||||
|
||||
def update_serial_no_reference(self, serial_no=None):
|
||||
def update_serial_no_reference(self, serial_no=None, sle=None):
|
||||
last_sle = self.get_last_sle(serial_no)
|
||||
|
||||
_last_sle_dict = last_sle.get("last_sle")
|
||||
if (
|
||||
_last_sle_dict
|
||||
and sle.get("voucher_type") != "Stock Reconciliation"
|
||||
and sle.get("voucher_no") != _last_sle_dict.get("voucher_no")
|
||||
and get_datetime(sle.get("posting_datetime"))
|
||||
< get_datetime(_last_sle_dict.get("posting_datetime"))
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"You can not complete this transaction because a future transaction exists for the serial number {0}"
|
||||
).format(serial_no)
|
||||
)
|
||||
|
||||
self.set_purchase_details(last_sle.get("purchase_sle"))
|
||||
self.set_sales_details(last_sle.get("delivery_sle"))
|
||||
self.set_maintenance_status()
|
||||
@@ -767,7 +796,7 @@ def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
|
||||
serial_no_doc.sales_order = None
|
||||
|
||||
serial_no_doc.validate_item()
|
||||
serial_no_doc.update_serial_no_reference(serial_no)
|
||||
serial_no_doc.update_serial_no_reference(serial_no, sle=args)
|
||||
|
||||
if is_new:
|
||||
serial_no_doc.db_insert()
|
||||
|
||||
@@ -458,17 +458,19 @@ class StockEntry(StockController):
|
||||
if acc_details.account_type == "Stock":
|
||||
frappe.throw(
|
||||
_(
|
||||
"At row {0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account"
|
||||
"At row #{0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account"
|
||||
).format(d.idx, get_link_to_form("Account", d.expense_account)),
|
||||
OpeningEntryAccountError,
|
||||
title=_("Difference Account in Items Table"),
|
||||
)
|
||||
|
||||
if self.purpose != "Material Issue" and acc_details.account_type == "Cost of Goods Sold":
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"At row {0}: You have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account"
|
||||
"At row #{0}: you have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account"
|
||||
).format(d.idx, bold(get_link_to_form("Account", d.expense_account))),
|
||||
title=_("Warning : Cost of Goods Sold Account"),
|
||||
title=_("Cost of Goods Sold Account in Items Table"),
|
||||
indicator="orange",
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def validate_warehouse(self):
|
||||
|
||||
@@ -489,6 +489,10 @@ class StockReconciliation(StockController):
|
||||
|
||||
self.update_inventory_dimensions(row, data)
|
||||
|
||||
if self.docstatus == 1 and has_dimensions and not row.batch_no:
|
||||
data.qty_after_transaction = data.actual_qty
|
||||
data.actual_qty = 0.0
|
||||
|
||||
return data
|
||||
|
||||
def make_sle_on_cancel(self):
|
||||
|
||||
@@ -159,10 +159,11 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None:
|
||||
|
||||
def get_item_groups_map(svd_list: SVDList) -> dict[str, str]:
|
||||
item_codes = set(i["item_code"] for i in svd_list)
|
||||
ig_list = frappe.get_list(
|
||||
"Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)]
|
||||
return frappe._dict(
|
||||
frappe.get_all(
|
||||
"Item", fields=["name", "item_group"], filters=[("name", "in", item_codes)], as_list=True
|
||||
)
|
||||
)
|
||||
return {i["item_code"]: i["item_group"] for i in ig_list}
|
||||
|
||||
|
||||
def get_item_groups_dict() -> ItemGroupsDict:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.query_builder.functions import IfNull, Max
|
||||
from frappe.utils import flt
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
@@ -208,30 +208,21 @@ def get_stock_ledger_entries(filters, items):
|
||||
if not items:
|
||||
return []
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
sle2 = frappe.qb.DocType("Stock Ledger Entry")
|
||||
max_posting_datetime_query = get_item_wise_max_posting_datetime(filters, items)
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.left_join(sle2)
|
||||
.join(max_posting_datetime_query)
|
||||
.on(
|
||||
(sle.item_code == sle2.item_code)
|
||||
& (sle.warehouse == sle2.warehouse)
|
||||
& (sle.posting_datetime < sle2.posting_datetime)
|
||||
& (sle.name < sle2.name)
|
||||
(sle.item_code == max_posting_datetime_query.item_code)
|
||||
& (sle.warehouse == max_posting_datetime_query.warehouse)
|
||||
& (sle.posting_datetime == max_posting_datetime_query.posting_datetime)
|
||||
)
|
||||
.select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company)
|
||||
.where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(items)))
|
||||
.where(sle.is_cancelled == 0)
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(sle.company == filters.get("company"))
|
||||
|
||||
if date := filters.get("date"):
|
||||
query = query.where(sle.posting_date <= date)
|
||||
else:
|
||||
frappe.throw(_("'Date' is required"))
|
||||
|
||||
if filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
@@ -247,4 +238,44 @@ def get_stock_ledger_entries(filters, items):
|
||||
)
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(sle.company == filters.get("company"))
|
||||
|
||||
if filters.get("data"):
|
||||
query = query.where(sle.posting_date <= filters.get("data"))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_item_wise_max_posting_datetime(filters, items):
|
||||
"""Get the maximum Stock Ledger Entry name for the given filters and items."""
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.item_code, sle.warehouse, sle.name, Max(sle.posting_datetime).as_("posting_datetime"))
|
||||
.where(sle.item_code.isin(items) & (sle.is_cancelled == 0))
|
||||
.groupby(sle.item_code, sle.warehouse)
|
||||
)
|
||||
|
||||
if filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
|
||||
if warehouse_details:
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
sle.warehouse.isin(
|
||||
frappe.qb.from_(wh)
|
||||
.select(wh.name)
|
||||
.where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt))
|
||||
)
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(sle.company == filters.get("company"))
|
||||
|
||||
if filters.get("data"):
|
||||
query = query.where(sle.posting_date <= filters.get("data"))
|
||||
|
||||
return query
|
||||
|
||||
@@ -655,8 +655,11 @@ class update_entries_after:
|
||||
if not sle.is_adjustment_entry:
|
||||
sle.stock_value_difference = stock_value_difference
|
||||
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
|
||||
sle.stock_value_difference = get_stock_value_difference(
|
||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
||||
sle.stock_value_difference = (
|
||||
get_stock_value_difference(
|
||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
||||
)
|
||||
* -1
|
||||
)
|
||||
|
||||
sle.doctype = "Stock Ledger Entry"
|
||||
|
||||
@@ -109,6 +109,8 @@ def get_stock_balance(
|
||||
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
|
||||
frappe.has_permission("Item", "read", throw=True)
|
||||
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
if posting_time is None:
|
||||
|
||||
Reference in New Issue
Block a user