Merge branch 'develop' into fix-voucher-types

This commit is contained in:
Smit Vora
2024-11-07 13:11:55 +05:30
committed by GitHub
670 changed files with 389420 additions and 376772 deletions

View File

@@ -148,14 +148,16 @@ class AccountsController(TransactionBase):
def ensure_supplier_is_not_blocked(self):
is_supplier_payment = self.doctype == "Payment Entry" and self.party_type == "Supplier"
is_buying_invoice = self.doctype in ["Purchase Invoice", "Purchase Order"]
supplier_name = self.supplier if is_buying_invoice else self.party if is_supplier_payment else None
supplier = None
supplier_name = None
if is_buying_invoice or is_supplier_payment:
supplier_name = self.supplier if is_buying_invoice else self.party
supplier = frappe.get_doc("Supplier", supplier_name)
if supplier_name:
supplier = frappe.get_doc(
"Supplier",
supplier_name,
)
if supplier and supplier_name and supplier.on_hold:
if supplier and supplier.on_hold:
if (is_buying_invoice and supplier.hold_type in ["All", "Invoices"]) or (
is_supplier_payment and supplier.hold_type in ["All", "Payments"]
):
@@ -345,9 +347,22 @@ class AccountsController(TransactionBase):
repost_doc.flags.ignore_links = True
repost_doc.save(ignore_permissions=True)
def _remove_advance_payment_ledger_entries(self):
adv = qb.DocType("Advance Payment Ledger Entry")
qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if self.doctype in advance_payment_doctypes:
qb.from_(adv).delete().where(
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
).run()
def on_trash(self):
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
self._remove_advance_payment_ledger_entries()
self._remove_references_in_repost_doctypes()
self._remove_references_in_unreconcile()
self.remove_serial_and_batch_bundle()
@@ -393,12 +408,15 @@ class AccountsController(TransactionBase):
def validate_return_against_account(self):
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To"
cr_dr_account = self.get(cr_dr_account_field)
if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account:
original_account = frappe.get_value(self.doctype, self.return_against, cr_dr_account_field)
if original_account != self.get(cr_dr_account_field):
frappe.throw(
_("'{0}' account: '{1}' should match the Return Against Invoice").format(
frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account)
_(
"Please set {0} to {1}, the same account that was used in the original invoice {2}."
).format(
frappe.bold(_(self.meta.get_label(cr_dr_account_field), context=self.doctype)),
frappe.bold(original_account),
frappe.bold(self.return_against),
)
)
@@ -448,6 +466,11 @@ class AccountsController(TransactionBase):
)
def validate_invoice_documents_schedule(self):
if self.is_return:
self.payment_terms_template = ""
self.payment_schedule = []
return
self.validate_payment_schedule_dates()
self.set_due_date()
self.set_payment_schedule()
@@ -462,7 +485,7 @@ class AccountsController(TransactionBase):
self.validate_payment_schedule_amount()
def validate_all_documents_schedule(self):
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return:
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
self.validate_invoice_documents_schedule()
elif self.doctype in ("Quotation", "Purchase Order", "Sales Order"):
self.validate_non_invoice_documents_schedule()
@@ -1039,7 +1062,9 @@ class AccountsController(TransactionBase):
gl_dict.update(
{
"transaction_currency": self.get("currency") or self.company_currency,
"transaction_exchange_rate": self.get("conversion_rate", 1),
"transaction_exchange_rate": item.get("exchange_rate", 1)
if self.doctype == "Journal Entry" and item
else self.get("conversion_rate", 1),
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
account_currency, gl_dict, "debit"
),
@@ -1064,6 +1089,13 @@ class AccountsController(TransactionBase):
"Stock Entry": "stock_entry_type",
"Asset Capitalization": "entry_type",
}
for method_name in frappe.get_hooks("voucher_subtypes"):
voucher_subtype = frappe.get_attr(method_name)(self)
if voucher_subtype:
return voucher_subtype
if self.doctype in voucher_subtypes:
return self.get(voucher_subtypes[self.doctype])
elif self.doctype == "Purchase Receipt" and self.is_return:
@@ -1076,6 +1108,7 @@ class AccountsController(TransactionBase):
return "Debit Note"
elif self.doctype == "Purchase Invoice" and self.is_return:
return "Debit Note"
return self.doctype
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
@@ -1909,25 +1942,22 @@ class AccountsController(TransactionBase):
return stock_items
def set_total_advance_paid(self):
ple = frappe.qb.DocType("Payment Ledger Entry")
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
party = self.customer
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
party = self.supplier
def calculate_total_advance_from_ledger(self):
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
advance = (
frappe.qb.from_(ple)
.select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount"))
frappe.qb.from_(adv)
.select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount"))
.where(
(ple.against_voucher_type == self.doctype)
& (ple.against_voucher_no == self.name)
& (ple.party == party)
& (ple.delinked == 0)
& (ple.company == self.company)
(adv.against_voucher_type == self.doctype)
& (adv.against_voucher_no == self.name)
& (adv.company == self.company)
)
.run(as_dict=True)
)
return advance
def set_total_advance_paid(self):
advance = self.calculate_total_advance_from_ledger()
advance_paid, order_total = None, None
if advance:
@@ -1968,33 +1998,24 @@ class AccountsController(TransactionBase):
def set_advance_payment_status(self):
new_status = None
stati = frappe.get_all(
"Payment Request",
{
paid_amount = frappe.get_value(
doctype="Payment Request",
filters={
"reference_doctype": self.doctype,
"reference_name": self.name,
"docstatus": 1,
},
pluck="status",
fieldname="sum(grand_total - outstanding_amount)",
)
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
if not stati:
new_status = "Not Requested"
elif "Requested" in stati or "Failed" in stati:
new_status = "Requested"
elif "Partially Paid" in stati:
new_status = "Partially Paid"
elif "Paid" in stati:
new_status = "Fully Paid"
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
if not stati:
new_status = "Not Initiated"
elif "Initiated" in stati or "Failed" in stati or "Payment Ordered" in stati:
new_status = "Initiated"
elif "Partially Paid" in stati:
new_status = "Partially Paid"
elif "Paid" in stati:
new_status = "Fully Paid"
if not paid_amount:
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
new_status = "Not Requested" if paid_amount is None else "Requested"
elif self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
new_status = "Not Initiated" if paid_amount is None else "Initiated"
else:
total_amount = self.get("rounded_total") or self.get("grand_total")
new_status = "Fully Paid" if paid_amount == total_amount else "Partially Paid"
if new_status == self.advance_payment_status:
return
@@ -2179,8 +2200,8 @@ class AccountsController(TransactionBase):
date = self.get("due_date")
due_date = date or posting_date
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
base_grand_total = flt(self.get("base_rounded_total") or self.base_grand_total)
grand_total = flt(self.get("rounded_total") or self.grand_total)
automatically_fetch_payment_terms = 0
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
@@ -2254,6 +2275,8 @@ class AccountsController(TransactionBase):
self.ignore_default_payment_terms_template = 1
def get_order_details(self):
if not self.get("items"):
return None, None, None
if self.doctype == "Sales Invoice":
po_or_so = self.get("items")[0].get("sales_order")
po_or_so_doctype = "Sales Order"
@@ -2364,8 +2387,8 @@ class AccountsController(TransactionBase):
total += flt(d.payment_amount, d.precision("payment_amount"))
base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
base_grand_total = flt(self.get("base_rounded_total") or self.base_grand_total)
grand_total = flt(self.get("rounded_total") or self.grand_total)
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
@@ -2565,6 +2588,69 @@ class AccountsController(TransactionBase):
repost_ledger.insert()
repost_ledger.submit()
def get_advance_payment_doctypes(self) -> list:
return frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
def make_advance_payment_ledger_for_journal(self):
advance_payment_doctypes = self.get_advance_payment_doctypes()
advance_doctype_references = [
x for x in self.accounts if x.reference_type in advance_payment_doctypes
]
for x in advance_doctype_references:
# Looking for payments
dr_or_cr = (
"credit_in_account_currency"
if x.account_type == "Receivable"
else "debit_in_account_currency"
)
amount = x.get(dr_or_cr)
if amount > 0:
doc = frappe.new_doc("Advance Payment Ledger Entry")
doc.company = self.company
doc.voucher_type = self.doctype
doc.voucher_no = self.name
doc.against_voucher_type = x.reference_type
doc.against_voucher_no = x.reference_name
doc.amount = amount if self.docstatus == 1 else -1 * amount
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
doc.currency = x.account_currency
doc.flags.ignore_permissions = 1
doc.save()
def make_advance_payment_ledger_for_payment(self):
advance_payment_doctypes = self.get_advance_payment_doctypes()
advance_doctype_references = [
x for x in self.references if x.reference_doctype in advance_payment_doctypes
]
currency = (
self.paid_from_account_currency
if self.payment_type == "Receive"
else self.paid_to_account_currency
)
for x in advance_doctype_references:
doc = frappe.new_doc("Advance Payment Ledger Entry")
doc.company = self.company
doc.voucher_type = self.doctype
doc.voucher_no = self.name
doc.against_voucher_type = x.reference_doctype
doc.against_voucher_no = x.reference_name
doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount
doc.currency = currency
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
doc.flags.ignore_permissions = 1
doc.save()
def make_advance_payment_ledger_entries(self):
if self.docstatus != 0:
if self.doctype == "Journal Entry":
self.make_advance_payment_ledger_for_journal()
elif self.doctype == "Payment Entry":
self.make_advance_payment_ledger_for_payment()
@frappe.whitelist()
def get_tax_rate(account_head):
@@ -2648,21 +2734,21 @@ def validate_taxes_and_charges(tax):
tax.rate = None
def validate_account_head(idx, account, company, context=""):
account_company = frappe.get_cached_value("Account", account, "company")
is_group = frappe.get_cached_value("Account", account, "is_group")
if account_company != company:
def validate_account_head(idx: int, account: str, company: str, context: str | None = None) -> None:
"""Throw a ValidationError if the account belongs to a different company or is a group account."""
if company != frappe.get_cached_value("Account", account, "company"):
frappe.throw(
_("Row {0}: {3} Account {1} does not belong to Company {2}").format(
idx, frappe.bold(account), frappe.bold(company), context
_("Row {0}: The {3} Account {1} does not belong to the company {2}").format(
idx, frappe.bold(account), frappe.bold(company), context or ""
),
title=_("Invalid Account"),
)
if is_group:
if frappe.get_cached_value("Account", account, "is_group"):
frappe.throw(
_("Row {0}: Account {1} is a Group Account").format(idx, frappe.bold(account)),
_(
"You selected the account group {1} as {2} Account in row {0}. Please select a single account."
).format(idx, frappe.bold(account), context or ""),
title=_("Invalid Account"),
)
@@ -3344,7 +3430,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
items_added_or_removed = False # updated to true if any new item is added or removed
any_conversion_factor_changed = False
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_doc_permissions(parent, "write")
@@ -3460,25 +3545,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
if parent_doctype in sales_doctypes:
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
if parent_doctype in sales_doctypes:
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag:
@@ -3553,6 +3634,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_billing_percentage()
parent.set_status()
parent.validate_uom_is_integer("uom", "qty")
parent.validate_uom_is_integer("stock_uom", "stock_qty")
# Cancel and Recreate Stock Reservation Entries.
if parent_doctype == "Sales Order":
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (

View File

@@ -251,16 +251,16 @@ class BuyingController(SubcontractingController):
if self.meta.get_field("base_in_words"):
if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled():
amount = abs(self.base_rounded_total)
amount = abs(flt(self.base_rounded_total))
else:
amount = abs(self.base_grand_total)
amount = abs(flt(self.base_grand_total))
self.base_in_words = money_in_words(amount, self.company_currency)
if self.meta.get_field("in_words"):
if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled():
amount = abs(self.rounded_total)
amount = abs(flt(self.rounded_total))
else:
amount = abs(self.grand_total)
amount = abs(flt(self.grand_total))
self.in_words = money_in_words(amount, self.currency)
@@ -702,9 +702,11 @@ class BuyingController(SubcontractingController):
if self.get("is_return"):
return
if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
"Buying Settings", "disable_last_purchase_rate"
):
if self.doctype in [
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
update_last_purchase_rate(self, is_submit=0)
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:

View File

@@ -79,6 +79,9 @@ def validate_returned_items(doc):
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
select_fields += ",rejected_qty, received_qty"
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
select_fields += ",name"
for d in frappe.db.sql(
f"""select {select_fields} from `tab{doc.doctype} Item` where parent = %s""",
doc.return_against,
@@ -104,15 +107,24 @@ def validate_returned_items(doc):
items_returned = False
for d in doc.get("items"):
key = d.item_code
raise_exception = False
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
field = frappe.scrub(doc.doctype) + "_item"
if d.get(field):
key = (d.item_code, d.get(field))
raise_exception = True
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
if d.item_code not in valid_items:
frappe.throw(
if key not in valid_items:
frappe.msgprint(
_("Row # {0}: Returned Item {1} does not exist in {2} {3}").format(
d.idx, d.item_code, doc.doctype, doc.return_against
)
),
raise_exception=raise_exception,
)
else:
ref = valid_items.get(d.item_code, frappe._dict())
ref = valid_items.get(key, frappe._dict())
validate_quantity(doc, d, ref, valid_items, already_returned_items)
if (
@@ -193,8 +205,12 @@ def validate_quantity(doc, args, ref, valid_items, already_returned_items):
def get_ref_item_dict(valid_items, ref_item_row):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
key = ref_item_row.item_code
if ref_item_row.get("name"):
key = (ref_item_row.item_code, ref_item_row.name)
valid_items.setdefault(
ref_item_row.item_code,
key,
frappe._dict(
{
"qty": 0,
@@ -208,7 +224,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
}
),
)
item_dict = valid_items[ref_item_row.item_code]
item_dict = valid_items[key]
item_dict["qty"] += ref_item_row.qty
item_dict["stock_qty"] += ref_item_row.get("stock_qty", 0)
if ref_item_row.get("rate", 0) > item_dict["rate"]:
@@ -335,6 +351,9 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
if source.tax_withholding_category:
doc.set_onload("supplier_tds", source.tax_withholding_category)
elif doctype == "Delivery Note":
# manual additions to the return should hit the return warehous, too
doc.set_warehouse = default_warehouse_for_sales_return
for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual":
@@ -581,6 +600,10 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
if not source_doc.use_serial_batch_fields and source_doc.serial_and_batch_bundle:
target_doc.serial_no = None
target_doc.batch_no = None
if (
(source_doc.serial_no or source_doc.batch_no)
and not source_doc.serial_and_batch_bundle
@@ -883,6 +906,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
"`tabSerial and Batch Entry`.`serial_no`",
"`tabSerial and Batch Entry`.`batch_no`",
"`tabSerial and Batch Entry`.`qty`",
"`tabSerial and Batch Entry`.`incoming_rate`",
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
"`tabSerial and Batch Bundle`.`voucher_type`",
"`tabSerial and Batch Bundle`.`voucher_no`",
@@ -904,15 +928,23 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
if key not in available_dict:
available_dict[key] = frappe._dict(
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
{
"qty": 0.0,
"serial_nos": defaultdict(float),
"batches": defaultdict(float),
"serial_nos_valuation": defaultdict(float),
"batches_valuation": defaultdict(float),
}
)
available_dict[key]["qty"] += row.qty
if row.serial_no:
available_dict[key]["serial_nos"][row.serial_no] += row.qty
available_dict[key]["serial_nos_valuation"][row.serial_no] = row.incoming_rate
elif row.batch_no:
available_dict[key]["batches"][row.batch_no] += row.qty
available_dict[key]["batches_valuation"][row.batch_no] = row.incoming_rate
return available_dict
@@ -948,12 +980,13 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
)
)
else:
fields = [
"serial_and_batch_bundle",
]
fields = ["serial_and_batch_bundle"]
if is_rejected:
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
fields.append("rejected_serial_and_batch_bundle")
if doctype == "Purchase Receipt Item":
fields.append("return_qty_from_rejected_warehouse")
del filters["rejected_serial_and_batch_bundle"]
data = frappe.get_all(
@@ -987,7 +1020,14 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
warehouse = row.get(warehouse_field)
qty = abs(row.get(qty_field))
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
filterd_serial_batch = frappe._dict(
{
"serial_nos": [],
"batches": defaultdict(float),
"serial_nos_valuation": data.get("serial_nos_valuation"),
"batches_valuation": data.get("batches_valuation"),
}
)
if data.serial_nos:
available_serial_nos = []
@@ -997,7 +1037,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
if available_serial_nos:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
available_serial_nos = get_available_serial_nos(available_serial_nos)
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
if len(available_serial_nos) > qty:
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
@@ -1082,6 +1122,8 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
"warehouse": warehouse,
"serial_nos": data.get("serial_nos"),
"batches": data.get("batches"),
"serial_nos_valuation": data.get("serial_nos_valuation"),
"batches_valuation": data.get("batches_valuation"),
"posting_date": parent_doc.posting_date,
"posting_time": parent_doc.posting_time,
"voucher_type": parent_doc.doctype,

View File

@@ -470,6 +470,16 @@ class SellingController(StockController):
raise_error_if_no_rate=False,
)
if (
not d.incoming_rate
and self.get("return_against")
and self.get("is_return")
and get_valuation_method(d.item_code) == "Moving Average"
):
d.incoming_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
if self.doctype == "Delivery Note" or self.get("update_stock"):

View File

@@ -63,6 +63,33 @@ class StockController(AccountsController):
self.set_rate_of_stock_uom()
self.validate_internal_transfer()
self.validate_putaway_capacity()
self.reset_conversion_factor()
def reset_conversion_factor(self):
for row in self.get("items"):
if row.uom != row.stock_uom:
continue
if row.conversion_factor != 1.0:
row.conversion_factor = 1.0
frappe.msgprint(
_(
"Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}."
).format(bold(row.item_code), bold(row.uom), bold(row.stock_uom)),
alert=True,
)
def validate_items_exist(self):
if not self.get("items"):
return
items = [d.item_code for d in self.get("items")]
exists_items = frappe.get_all("Item", filters={"name": ("in", items)}, pluck="name")
non_exists_items = set(items) - set(exists_items)
if non_exists_items:
frappe.throw(_("Items {0} do not exist in the Item master.").format(", ".join(non_exists_items)))
def validate_duplicate_serial_and_batch_bundle(self, table_name):
if not self.get(table_name):
@@ -307,6 +334,11 @@ class StockController(AccountsController):
}
)
if self.doctype in ["Sales Invoice", "Delivery Note"]:
row.db_set(
"incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
)
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
field = {
"Sales Invoice": "sales_invoice_item",
@@ -345,6 +377,9 @@ class StockController(AccountsController):
@frappe.request_cache
def is_serial_batch_item(self, item_code) -> bool:
if not frappe.db.exists("Item", item_code):
frappe.throw(_("Item {0} does not exist.").format(bold(item_code)))
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
if item_details.has_serial_no or item_details.has_batch_no:

View File

@@ -561,11 +561,11 @@ class SubcontractingController(StockController):
use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
if self.doctype == self.subcontract_data.order_doctype:
rm_obj.required_qty = qty
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
rm_obj.required_qty = flt(qty, rm_obj.precision("required_qty"))
rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount"))
else:
rm_obj.consumed_qty = qty
rm_obj.required_qty = bom_item.required_qty or qty
rm_obj.consumed_qty = flt(qty, rm_obj.precision("consumed_qty"))
rm_obj.required_qty = flt(bom_item.required_qty or qty, rm_obj.precision("required_qty"))
rm_obj.serial_and_batch_bundle = None
setattr(
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
@@ -576,30 +576,56 @@ class SubcontractingController(StockController):
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
args = frappe._dict(
{
"item_code": rm_obj.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty),
"actual_qty": -1 * flt(rm_obj.consumed_qty),
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": item_row.name,
"company": self.company,
"allow_zero_valuation": 1,
}
)
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
item_row, rm_obj, rm_obj.consumed_qty
)
if rm_obj.serial_and_batch_bundle:
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
self.set_rate_for_supplied_items(rm_obj, item_row)
rm_obj.rate = get_incoming_rate(args)
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":
return
for row in self.supplied_items:
item_row = None
if row.reference_name:
item_row = self.get_item_row(row.reference_name)
if not item_row:
continue
self.set_rate_for_supplied_items(row, item_row)
def get_item_row(self, reference_name):
for item in self.items:
if item.name == reference_name:
return item
def set_rate_for_supplied_items(self, rm_obj, item_row):
args = frappe._dict(
{
"item_code": rm_obj.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty),
"actual_qty": -1 * flt(rm_obj.consumed_qty),
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": item_row.name,
"company": self.company,
"allow_zero_valuation": 1,
}
)
if rm_obj.serial_and_batch_bundle:
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
if rm_obj.use_serial_batch_fields:
args["batch_no"] = rm_obj.batch_no
args["serial_no"] = rm_obj.serial_no
rm_obj.rate = get_incoming_rate(args)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
@@ -638,8 +664,8 @@ class SubcontractingController(StockController):
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
rm_obj.required_qty = flt(required_qty, rm_obj.precision("required_qty"))
rm_obj.consumed_qty = flt(consumed_qty, rm_obj.precision("consumed_qty"))
def __set_serial_nos(self, item_row, rm_obj):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
@@ -1209,6 +1235,17 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
def make_return_stock_entry_for_subcontract(
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
):
def post_process(source_doc, target_doc):
target_doc.purpose = "Material Transfer"
if source_doc.doctype == "Purchase Order":
target_doc.purchase_order = source_doc.name
else:
target_doc.subcontracting_order = source_doc.name
target_doc.company = source_doc.company
target_doc.is_return = 1
ste_doc = get_mapped_doc(
order_doctype,
order_doc.name,
@@ -1219,18 +1256,13 @@ def make_return_stock_entry_for_subcontract(
},
},
ignore_child_tables=True,
postprocess=post_process,
)
ste_doc.purpose = "Material Transfer"
if order_doctype == "Purchase Order":
ste_doc.purchase_order = order_doc.name
rm_detail_field = "po_detail"
else:
ste_doc.subcontracting_order = order_doc.name
rm_detail_field = "sco_rm_detail"
ste_doc.company = order_doc.company
ste_doc.is_return = 1
for _key, value in available_materials.items():
if not value.qty:

View File

@@ -41,7 +41,7 @@ class calculate_taxes_and_totals:
return items
def calculate(self):
if not len(self._items):
if not len(self.doc.items):
return
self.discount_amount_applied = False
@@ -96,7 +96,7 @@ class calculate_taxes_and_totals:
if self.doc.get("is_return") and self.doc.get("return_against"):
return
for item in self._items:
for item in self.doc.items:
if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
@@ -155,7 +155,7 @@ class calculate_taxes_and_totals:
return
if not self.discount_amount_applied:
for item in self._items:
for item in self.doc.items:
self.doc.round_floats_in(item)
if item.discount_percentage == 100:
@@ -259,7 +259,7 @@ class calculate_taxes_and_totals:
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return
for item in self._items:
for item in self.doc.items:
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0
@@ -514,7 +514,7 @@ class calculate_taxes_and_totals:
if tax.item_wise_tax_detail.get(key):
item_wise_tax_amount += tax.item_wise_tax_detail[key][1]
tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)]
tax.item_wise_tax_detail[key] = [tax_rate, item_wise_tax_amount]
def round_off_totals(self, tax):
if tax.account_head in frappe.flags.round_off_applicable_accounts:

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -46,7 +46,7 @@ def make_supplier(supplier_name, currency=None):
return supplier_name
class TestAccountsController(FrappeTestCase):
class TestAccountsController(IntegrationTestCase):
"""
Test Exchange Gain/Loss booking on various scenarios.
Test Cases are numbered for better organization
@@ -805,7 +805,9 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
@change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1})
@IntegrationTestCase.change_settings(
"Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1}
)
def test_16_internal_transfer_at_arms_length_price(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

View File

@@ -1,11 +1,11 @@
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestTaxesAndTotals(AccountsTestMixin, FrappeTestCase):
class TestTaxesAndTotals(AccountsTestMixin, IntegrationTestCase):
def test_distributed_discount_amount(self):
so = make_sales_order(do_not_save=1)
so.apply_discount_on = "Net Total"

View File

@@ -2,6 +2,7 @@ import json
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
from erpnext.stock.doctype.item.test_item import set_item_variant_settings
@@ -10,7 +11,7 @@ from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
)
class TestItemVariant(unittest.TestCase):
class TestItemVariant(IntegrationTestCase):
def test_tables_in_template_copied_to_variant(self):
fields = [{"field_name": "quality_inspection_template"}]
set_item_variant_settings(fields)

View File

@@ -4,15 +4,16 @@ import unittest
import frappe
import frappe.utils
from frappe.model import mapper
from frappe.test_runner import make_test_records
from frappe.tests import IntegrationTestCase
from frappe.utils import add_months, nowdate
EXTRA_TEST_RECORD_DEPENDENCIES = ["Item"]
class TestMapper(unittest.TestCase):
class TestMapper(IntegrationTestCase):
def test_map_docs(self):
"""Test mapping of multiple source docs on a single target doc"""
make_test_records("Item")
items = ["_Test Item", "_Test Item 2", "_Test FG Item"]
# Make source docs (quotations) and a target doc (sales order)

View File

@@ -2,13 +2,14 @@ import unittest
from uuid import uuid4 as _uuid4
import frappe
from frappe.tests import IntegrationTestCase
def uuid4():
return str(_uuid4())
class TestTaxes(unittest.TestCase):
class TestTaxes(IntegrationTestCase):
def setUp(self):
self.company = frappe.get_doc(
{

View File

@@ -2,6 +2,7 @@ import unittest
from functools import partial
import frappe
from frappe.tests import IntegrationTestCase
from erpnext.controllers import queries
@@ -10,8 +11,11 @@ def add_default_params(func, doctype):
return partial(func, doctype=doctype, txt="", searchfield="name", start=0, page_len=20, filters=None)
class TestQueries(unittest.TestCase):
# All tests are based on doctype/test_records.json
EXTRA_TEST_RECORD_DEPENDENCIES = ["Employee", "Lead", "Item", "BOM", "Project", "Account"]
class TestQueries(IntegrationTestCase):
# All tests are based on self.globalTestRecords[doctype]
def assert_nested_in(self, item, container):
self.assertIn(item, [vals for tuples in container for vals in tuples])

View File

@@ -5,7 +5,7 @@ import copy
from collections import defaultdict
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests import IntegrationTestCase
from frappe.utils import cint
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -25,7 +25,7 @@ from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order im
)
class TestSubcontractingController(FrappeTestCase):
class TestSubcontractingController(IntegrationTestCase):
def setUp(self):
make_subcontracted_items()
make_raw_materials()
@@ -1234,6 +1234,7 @@ def make_subcontracted_items():
"Subcontracted Item SA6": {},
"Subcontracted Item SA7": {},
"Subcontracted Item SA8": {},
"Subcontracted Item SA9": {"stock_uom": "Litre"},
}
for item, properties in sub_contracted_items.items():
@@ -1254,6 +1255,7 @@ def make_raw_materials():
"Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
"Subcontracted SRM Item 8": {},
"Subcontracted SRM Item 9": {"stock_uom": "Litre"},
}
for item, properties in raw_materials.items():
@@ -1280,6 +1282,7 @@ def make_service_items():
"Subcontracted Service Item 6": {},
"Subcontracted Service Item 7": {},
"Subcontracted Service Item 8": {},
"Subcontracted Service Item 9": {},
}
for item, properties in service_items.items():

View File

@@ -1,9 +1,10 @@
import unittest
import frappe
from frappe.tests import IntegrationTestCase
class TestUtils(unittest.TestCase):
class TestUtils(IntegrationTestCase):
def test_reset_default_field_value(self):
doc = frappe.get_doc(
{

View File

@@ -69,7 +69,7 @@ def get_data(filters, conditions):
"Delivery Note",
]:
posting_date = "t1.posting_date"
if filters.period_based_on:
if filters.period_based_on and conditions.get("trans") in ["Sales Invoice", "Purchase Invoice"]:
posting_date = "t1." + filters.period_based_on
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
@@ -224,7 +224,7 @@ def period_wise_columns_query(filters, trans):
if trans in ["Purchase Receipt", "Delivery Note", "Purchase Invoice", "Sales Invoice"]:
trans_date = "posting_date"
if filters.period_based_on:
if filters.period_based_on and trans in ["Purchase Invoice", "Sales Invoice"]:
trans_date = filters.period_based_on
else:
trans_date = "transaction_date"