Merge branch 'develop' into remove-je-list-indicators

This commit is contained in:
barredterra 2024-01-11 14:03:13 +01:00
commit e69f9ddf8b
208 changed files with 1486859 additions and 565941 deletions

View File

@ -91,8 +91,8 @@ class Account(NestedSet):
super(Account, self).on_update() super(Account, self).on_update()
def onload(self): def onload(self):
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier" "Accounts Settings", "frozen_accounts_modifier"
) )
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles(): if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
self.set_onload("can_freeze_account", True) self.set_onload("can_freeze_account", True)

View File

@ -77,7 +77,7 @@ frappe.treeview_settings["Account"] = {
// show Dr if positive since balance is calculated as debit - credit else show Cr // show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance; const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? "Dr": "Cr"; const dr_or_cr = balance > 0 ? __("Dr"): __("Cr");
const format = (value, currency) => format_currency(Math.abs(value), currency); const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) { if (account.balance!==undefined) {

View File

@ -231,6 +231,8 @@ def build_account_tree(tree, parent, all_accounts):
tree[child.account_name]["account_type"] = child.account_type tree[child.account_name]["account_type"] = child.account_type
if child.tax_rate: if child.tax_rate:
tree[child.account_name]["tax_rate"] = child.tax_rate tree[child.account_name]["tax_rate"] = child.tax_rate
if child.account_currency:
tree[child.account_name]["account_currency"] = child.account_currency
if not parent: if not parent:
tree[child.account_name]["root_type"] = child.root_type tree[child.account_name]["root_type"] = child.root_type

View File

@ -444,6 +444,10 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
vouchers = json.loads(vouchers) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers) transaction.add_payment_entries(vouchers)
transaction.validate_duplicate_references()
transaction.allocate_payment_entries()
transaction.update_allocated_amount()
transaction.set_status()
transaction.save() transaction.save()
return transaction return transaction

View File

@ -3,12 +3,11 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document
from frappe.utils import flt from frappe.utils import flt
from erpnext.controllers.status_updater import StatusUpdater
class BankTransaction(Document):
class BankTransaction(StatusUpdater):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # This code is auto-generated. Do not modify anything in this block.
@ -50,6 +49,15 @@ class BankTransaction(StatusUpdater):
def validate(self): def validate(self):
self.validate_duplicate_references() self.validate_duplicate_references()
def set_status(self):
if self.docstatus == 2:
self.db_set("status", "Cancelled")
elif self.docstatus == 1:
if self.unallocated_amount > 0:
self.db_set("status", "Unreconciled")
elif self.unallocated_amount <= 0:
self.db_set("status", "Reconciled")
def validate_duplicate_references(self): def validate_duplicate_references(self):
"""Make sure the same voucher is not allocated twice within the same Bank Transaction""" """Make sure the same voucher is not allocated twice within the same Bank Transaction"""
if not self.payment_entries: if not self.payment_entries:
@ -83,12 +91,13 @@ class BankTransaction(StatusUpdater):
self.validate_duplicate_references() self.validate_duplicate_references()
self.allocate_payment_entries() self.allocate_payment_entries()
self.update_allocated_amount() self.update_allocated_amount()
self.set_status()
def on_cancel(self): def on_cancel(self):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel=True) self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.set_status(update=True) self.set_status()
def add_payment_entries(self, vouchers): def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance" "Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
@ -366,15 +375,17 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
and len(get_reconciled_bank_transactions(doctype, docname)) < 2 and len(get_reconciled_bank_transactions(doctype, docname)) < 2
): ):
return return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Sales Invoice": if doctype == "Sales Invoice":
frappe.db.set_value( frappe.db.set_value(
"Sales Invoice Payment", "Sales Invoice Payment",
dict(parenttype=doctype, parent=docname), dict(parenttype=doctype, parent=docname),
"clearance_date", "clearance_date",
clearance_date, clearance_date,
) )
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Bank Transaction": elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund # For when a second bank transaction has fixed another, e.g. refund

View File

@ -435,8 +435,8 @@ def update_outstanding_amt(
def validate_frozen_account(account, adv_adj=None): def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account") frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == "Yes" and not adv_adj: if frozen_account == "Yes" and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", None, "frozen_accounts_modifier" "Accounts Settings", "frozen_accounts_modifier"
) )
if not frozen_accounts_modifier: if not frozen_accounts_modifier:

View File

@ -747,6 +747,10 @@ frappe.ui.form.on('Payment Entry', {
args["get_orders_to_be_billed"] = true; args["get_orders_to_be_billed"] = true;
} }
if (frm.doc.book_advance_payments_in_separate_party_account) {
args["book_advance_payments_in_separate_party_account"] = true;
}
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount']; frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({ return frappe.call({

View File

@ -223,6 +223,7 @@
"fieldname": "party_balance", "fieldname": "party_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Party Balance", "label": "Party Balance",
"no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -759,7 +760,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2023-11-23 12:07:20.887885", "modified": "2024-01-08 13:17:15.744754",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@ -50,6 +50,88 @@ class InvalidPaymentEntry(ValidationError):
class PaymentEntry(AccountsController): class PaymentEntry(AccountsController):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.advance_taxes_and_charges.advance_taxes_and_charges import (
AdvanceTaxesandCharges,
)
from erpnext.accounts.doctype.payment_entry_deduction.payment_entry_deduction import (
PaymentEntryDeduction,
)
from erpnext.accounts.doctype.payment_entry_reference.payment_entry_reference import (
PaymentEntryReference,
)
amended_from: DF.Link | None
apply_tax_withholding_amount: DF.Check
auto_repeat: DF.Link | None
bank: DF.ReadOnly | None
bank_account: DF.Link | None
bank_account_no: DF.ReadOnly | None
base_paid_amount: DF.Currency
base_paid_amount_after_tax: DF.Currency
base_received_amount: DF.Currency
base_received_amount_after_tax: DF.Currency
base_total_allocated_amount: DF.Currency
base_total_taxes_and_charges: DF.Currency
book_advance_payments_in_separate_party_account: DF.Check
clearance_date: DF.Date | None
company: DF.Link
contact_email: DF.Data | None
contact_person: DF.Link | None
cost_center: DF.Link | None
custom_remarks: DF.Check
deductions: DF.Table[PaymentEntryDeduction]
difference_amount: DF.Currency
letter_head: DF.Link | None
mode_of_payment: DF.Link | None
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
paid_amount: DF.Currency
paid_amount_after_tax: DF.Currency
paid_from: DF.Link
paid_from_account_balance: DF.Currency
paid_from_account_currency: DF.Link
paid_from_account_type: DF.Data | None
paid_to: DF.Link
paid_to_account_balance: DF.Currency
paid_to_account_currency: DF.Link
paid_to_account_type: DF.Data | None
party: DF.DynamicLink | None
party_balance: DF.Currency
party_bank_account: DF.Link | None
party_name: DF.Data | None
party_type: DF.Link | None
payment_order: DF.Link | None
payment_order_status: DF.Literal["Initiated", "Payment Ordered"]
payment_type: DF.Literal["Receive", "Pay", "Internal Transfer"]
posting_date: DF.Date
print_heading: DF.Link | None
project: DF.Link | None
purchase_taxes_and_charges_template: DF.Link | None
received_amount: DF.Currency
received_amount_after_tax: DF.Currency
reference_date: DF.Date | None
reference_no: DF.Data | None
references: DF.Table[PaymentEntryReference]
remarks: DF.SmallText | None
sales_taxes_and_charges_template: DF.Link | None
source_exchange_rate: DF.Float
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
target_exchange_rate: DF.Float
tax_withholding_category: DF.Link | None
taxes: DF.Table[AdvanceTaxesandCharges]
title: DF.Data | None
total_allocated_amount: DF.Currency
total_taxes_and_charges: DF.Currency
unallocated_amount: DF.Currency
# end: auto-generated types
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PaymentEntry, self).__init__(*args, **kwargs) super(PaymentEntry, self).__init__(*args, **kwargs)
if not self.is_new(): if not self.is_new():
@ -256,6 +338,7 @@ class PaymentEntry(AccountsController):
"get_outstanding_invoices": True, "get_outstanding_invoices": True,
"get_orders_to_be_billed": True, "get_orders_to_be_billed": True,
"vouchers": vouchers, "vouchers": vouchers,
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
}, },
validate=True, validate=True,
) )
@ -1628,11 +1711,16 @@ def get_outstanding_reference_documents(args, validate=False):
outstanding_invoices = [] outstanding_invoices = []
negative_outstanding_invoices = [] negative_outstanding_invoices = []
if args.get("book_advance_payments_in_separate_party_account"):
party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
else:
party_account = args.get("party_account")
if args.get("get_outstanding_invoices"): if args.get("get_outstanding_invoices"):
outstanding_invoices = get_outstanding_invoices( outstanding_invoices = get_outstanding_invoices(
args.get("party_type"), args.get("party_type"),
args.get("party"), args.get("party"),
get_party_account(args.get("party_type"), args.get("party"), args.get("company")), party_account,
common_filter=common_filter, common_filter=common_filter,
posting_date=posting_and_due_date, posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"), min_outstanding=args.get("outstanding_amt_greater_than"),

View File

@ -527,7 +527,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
values.extend(warehouses) values.extend(warehouses)
if items: if items:
condition = " and `tab{child_doc}`.{apply_on} in ({items})".format( condition += " and `tab{child_doc}`.{apply_on} in ({items})".format(
child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items)) child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items))
) )

View File

@ -296,6 +296,18 @@ class PurchaseInvoice(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
self.set_percentage_received()
def set_percentage_received(self):
total_billed_qty = 0.0
total_received_qty = 0.0
for row in self.items:
if row.purchase_receipt and row.pr_detail and row.received_qty:
total_billed_qty += row.qty
total_received_qty += row.received_qty
if total_billed_qty and total_received_qty:
self.per_received = total_received_qty / total_billed_qty * 100
def validate_release_date(self): def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date): if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@ -552,7 +564,7 @@ class PurchaseInvoice(BuyingController):
self.against_expense_account = ",".join(against_accounts) self.against_expense_account = ",".join(against_accounts)
def po_required(self): def po_required(self):
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes":
if frappe.get_value( if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
@ -572,7 +584,7 @@ class PurchaseInvoice(BuyingController):
def pr_required(self): def pr_required(self):
stock_items = self.get_stock_items() stock_items = self.get_stock_items()
if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes": if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes":
if frappe.get_value( if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt" "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
@ -1104,17 +1116,6 @@ class PurchaseInvoice(BuyingController):
item=item, item=item,
) )
) )
# update gross amount of asset bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
)
if ( if (
self.auto_accounting_for_stock self.auto_accounting_for_stock
and self.is_opening == "No" and self.is_opening == "No"
@ -1156,17 +1157,24 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount") item.item_tax_amount, item.precision("item_tax_amount")
) )
if item.is_fixed_asset and item.landed_cost_voucher_amount:
self.update_gross_purchase_amount_for_linked_assets(item)
def update_gross_purchase_amount_for_linked_assets(self, item):
assets = frappe.db.get_all( assets = frappe.db.get_all(
"Asset", "Asset",
filters={"purchase_invoice": self.name, "item_code": item.item_code}, filters={"purchase_invoice": self.name, "item_code": item.item_code},
fields=["name", "asset_quantity"], fields=["name", "asset_quantity"],
) )
for asset in assets: for asset in assets:
purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
frappe.db.set_value( frappe.db.set_value(
"Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity "Asset",
) asset.name,
frappe.db.set_value( {
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity "gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
},
) )
def make_stock_adjustment_entry( def make_stock_adjustment_entry(

View File

@ -14,7 +14,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.accounts_controller import InvalidQtyError, get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
@ -51,6 +51,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def test_purchase_invoice_qty(self):
pi = make_purchase_invoice(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
pi.save()
# No error with qty=1
pi.items[0].qty = 1
pi.save()
self.assertEqual(pi.items[0].qty, 1)
def test_purchase_invoice_received_qty(self): def test_purchase_invoice_received_qty(self):
""" """
1. Test if received qty is validated against accepted + rejected 1. Test if received qty is validated against accepted + rejected
@ -1227,11 +1237,11 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self): def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value( unlink_enabled = frappe.db.get_single_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" "Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
) )
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value( frappe.db.set_value(
@ -1422,7 +1432,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pay.cancel() pay.cancel()
frappe.db.set_single_value( frappe.db.set_single_value(
"Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled "Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled
) )
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
@ -2114,7 +2124,7 @@ def make_purchase_invoice(**args):
bundle_id = None bundle_id = None
if args.get("batch_no") or args.get("serial_no"): if args.get("batch_no") or args.get("serial_no"):
batches = {} batches = {}
qty = args.qty or 5 qty = args.qty if args.qty is not None else 5
item_code = args.item or args.item_code or "_Test Item" item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"): if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty}) batches = frappe._dict({args.batch_no: qty})
@ -2143,7 +2153,7 @@ def make_purchase_invoice(**args):
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"item_name": args.item_name, "item_name": args.item_name,
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 5, "qty": args.qty if args.qty is not None else 5,
"received_qty": args.received_qty or 0, "received_qty": args.received_qty or 0,
"rejected_qty": args.rejected_qty or 0, "rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50, "rate": args.rate or 50,

View File

@ -898,8 +898,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.events.append_time_log(frm, timesheet, 1.0); frm.events.append_time_log(frm, timesheet, 1.0);
} }
}); });
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals"); frm.trigger("calculate_timesheet_totals");
frm.refresh();
}, },
async get_exchange_rate(frm, from_currency, to_currency) { async get_exchange_rate(frm, from_currency, to_currency) {

View File

@ -23,7 +23,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_d
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule, get_depr_schedule,
) )
from erpnext.controllers.accounts_controller import update_invoice_status from erpnext.controllers.accounts_controller import InvalidQtyError, update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.selling.doctype.customer.test_customer import get_customer_dict from erpnext.selling.doctype.customer.test_customer import get_customer_dict
@ -72,6 +72,16 @@ class TestSalesInvoice(FrappeTestCase):
def tearDownClass(self): def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0) unlink_payment_on_cancel_of_invoice(0)
def test_sales_invoice_qty(self):
si = create_sales_invoice(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
si.save()
# No error with qty=1
si.items[0].qty = 1
si.save()
self.assertEqual(si.items[0].qty, 1)
def test_timestamp_change(self): def test_timestamp_change(self):
w = frappe.copy_doc(test_records[0]) w = frappe.copy_doc(test_records[0])
w.docstatus = 0 w.docstatus = 0
@ -3636,7 +3646,7 @@ def create_sales_invoice(**args):
bundle_id = None bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")): if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
batches = {} batches = {}
qty = args.qty or 1 qty = args.qty if args.qty is not None else 1
item_code = args.item or args.item_code or "_Test Item" item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"): if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty}) batches = frappe._dict({args.batch_no: qty})
@ -3668,7 +3678,7 @@ def create_sales_invoice(**args):
"description": args.description or "_Test Item", "description": args.description or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"target_warehouse": args.target_warehouse, "target_warehouse": args.target_warehouse,
"qty": args.qty or 1, "qty": args.qty if args.qty is not None else 1,
"uom": args.uom or "Nos", "uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos", "stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100, "rate": args.rate if args.get("rate") is not None else 100,

View File

@ -654,10 +654,10 @@ def check_freezing_date(posting_date, adv_adj=False):
Hence stop admin to bypass if accounts are freezed Hence stop admin to bypass if accounts are freezed
""" """
if not adv_adj: if not adv_adj:
acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto") acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
if acc_frozen_upto: if acc_frozen_upto:
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", None, "frozen_accounts_modifier" "Accounts Settings", "frozen_accounts_modifier"
) )
if getdate(posting_date) <= getdate(acc_frozen_upto) and ( if getdate(posting_date) <= getdate(acc_frozen_upto) and (
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator" frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"

View File

@ -55,8 +55,8 @@ class ReceivablePayableReport(object):
def run(self, args): def run(self, args):
self.filters.update(args) self.filters.update(args)
self.set_defaults() self.set_defaults()
self.party_naming_by = frappe.db.get_value( self.party_naming_by = frappe.db.get_single_value(
args.get("naming_by")[0], None, args.get("naming_by")[1] args.get("naming_by")[0], args.get("naming_by")[1]
) )
self.get_columns() self.get_columns()
self.get_data() self.get_data()

View File

@ -24,8 +24,8 @@ class AccountsReceivableSummary(ReceivablePayableReport):
def run(self, args): def run(self, args):
self.account_type = args.get("account_type") self.account_type = args.get("account_type")
self.party_type = get_party_types_from_account_type(self.account_type) self.party_type = get_party_types_from_account_type(self.account_type)
self.party_naming_by = frappe.db.get_value( self.party_naming_by = frappe.db.get_single_value(
args.get("naming_by")[0], None, args.get("naming_by")[1] args.get("naming_by")[0], args.get("naming_by")[1]
) )
self.get_columns() self.get_columns()
self.get_data(args) self.get_data(args)

View File

@ -128,7 +128,7 @@ frappe.query_reports["Consolidated Financial Statement"] = {
} }
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);
if (!data.parent_account) { if (data && !data.parent_account) {
value = $(`<span>${value}</span>`); value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold"); var $value = $(value).css("font-weight", "bold");

View File

@ -21,8 +21,8 @@ class PartyLedgerSummaryReport(object):
frappe.throw(_("From Date must be before To Date")) frappe.throw(_("From Date must be before To Date"))
self.filters.party_type = args.get("party_type") self.filters.party_type = args.get("party_type")
self.party_naming_by = frappe.db.get_value( self.party_naming_by = frappe.db.get_single_value(
args.get("naming_by")[0], None, args.get("naming_by")[1] args.get("naming_by")[0], args.get("naming_by")[1]
) )
self.get_gl_entries() self.get_gl_entries()

View File

@ -368,7 +368,7 @@ def filter_invoices_based_on_dimensions(filters, query, parent_doc):
dimension.document_type, filters.get(dimension.fieldname) dimension.document_type, filters.get(dimension.fieldname)
) )
fieldname = dimension.fieldname fieldname = dimension.fieldname
query = query.where(parent_doc[fieldname] == filters.fieldname) query = query.where(parent_doc[fieldname].isin(filters[fieldname]))
return query return query

View File

@ -1280,7 +1280,7 @@ def parse_naming_series_variable(doc, variable):
else: else:
date = getdate() date = getdate()
company = None company = None
return get_fiscal_year(date=date, company=company)[0] return get_fiscal_year(date=date, company=company).name
@frappe.whitelist() @frappe.whitelist()

View File

@ -571,10 +571,16 @@ frappe.ui.form.on('Asset', {
indicator: 'red' indicator: 'red'
}); });
} }
frm.set_value('gross_purchase_amount', item.base_net_rate + item.item_tax_amount); var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
frm.set_value('purchase_receipt_amount', item.base_net_rate + item.item_tax_amount); var asset_quantity = is_grouped_asset ? item.qty : 1;
item.asset_location && frm.set_value('location', item.asset_location); var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
frm.set_value('gross_purchase_amount', purchase_amount);
frm.set_value('purchase_receipt_amount', purchase_amount);
frm.set_value('asset_quantity', asset_quantity);
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center); frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
if(item.asset_location) { frm.set_value('location', item.asset_location); }
}, },
set_depreciation_rate: function(frm, row) { set_depreciation_rate: function(frm, row) {

View File

@ -202,9 +202,9 @@
"fieldname": "purchase_date", "fieldname": "purchase_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Purchase Date", "label": "Purchase Date",
"mandatory_depends_on": "eval:!doc.is_existing_asset",
"read_only": 1, "read_only": 1,
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
"reqd": 1
}, },
{ {
"fieldname": "disposal_date", "fieldname": "disposal_date",
@ -227,15 +227,15 @@
"fieldname": "gross_purchase_amount", "fieldname": "gross_purchase_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Gross Purchase Amount", "label": "Gross Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only_depends_on": "eval:!doc.is_existing_asset", "read_only_depends_on": "eval:!doc.is_existing_asset"
"reqd": 1
}, },
{ {
"fieldname": "available_for_use_date", "fieldname": "available_for_use_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Available-for-use Date", "label": "Available-for-use Date",
"reqd": 1 "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)"
}, },
{ {
"default": "0", "default": "0",
@ -590,7 +590,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2023-12-21 16:46:20.732869", "modified": "2024-01-05 17:36:53.131512",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -57,7 +57,7 @@ class Asset(AccountsController):
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"] asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
asset_owner_company: DF.Link | None asset_owner_company: DF.Link | None
asset_quantity: DF.Int asset_quantity: DF.Int
available_for_use_date: DF.Date available_for_use_date: DF.Date | None
booked_fixed_asset: DF.Check booked_fixed_asset: DF.Check
calculate_depreciation: DF.Check calculate_depreciation: DF.Check
capitalized_in: DF.Link | None capitalized_in: DF.Link | None
@ -92,7 +92,7 @@ class Asset(AccountsController):
number_of_depreciations_booked: DF.Int number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency opening_accumulated_depreciation: DF.Currency
policy_number: DF.Data | None policy_number: DF.Data | None
purchase_date: DF.Date purchase_date: DF.Date | None
purchase_invoice: DF.Link | None purchase_invoice: DF.Link | None
purchase_receipt: DF.Link | None purchase_receipt: DF.Link | None
purchase_receipt_amount: DF.Currency purchase_receipt_amount: DF.Currency
@ -316,7 +316,12 @@ class Asset(AccountsController):
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category): if is_cwip_accounting_enabled(self.asset_category):
if not self.is_existing_asset and not self.purchase_receipt and not self.purchase_invoice: if (
not self.is_existing_asset
and not self.is_composite_asset
and not self.purchase_receipt
and not self.purchase_invoice
):
frappe.throw( frappe.throw(
_("Please create purchase receipt or purchase invoice for the item {0}").format( _("Please create purchase receipt or purchase invoice for the item {0}").format(
self.item_code self.item_code
@ -329,7 +334,7 @@ class Asset(AccountsController):
and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock") and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock")
): ):
frappe.throw( frappe.throw(
_("Update stock must be enable for the purchase invoice {0}").format(self.purchase_invoice) _("Update stock must be enabled for the purchase invoice {0}").format(self.purchase_invoice)
) )
if not self.calculate_depreciation: if not self.calculate_depreciation:

View File

@ -19,6 +19,7 @@ from frappe.utils import (
) )
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, get_checks_for_pl_and_bs_accounts,
) )
@ -35,7 +36,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
def post_depreciation_entries(date=None): def post_depreciation_entries(date=None):
# Return if automatic booking of asset depreciation is disabled # Return if automatic booking of asset depreciation is disabled
if not cint( if not cint(
frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically") frappe.db.get_single_value("Accounts Settings", "book_asset_depreciation_entry_automatically")
): ):
return return
@ -522,6 +523,13 @@ def depreciate_asset(asset_doc, date, notes):
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
cancel_depreciation_entries(asset_doc, date)
@erpnext.allow_regional
def cancel_depreciation_entries(asset_doc, date):
pass
def reset_depreciation_schedule(asset_doc, date, notes): def reset_depreciation_schedule(asset_doc, date, notes):
if not asset_doc.calculate_depreciation: if not asset_doc.calculate_depreciation:

View File

@ -86,12 +86,12 @@ class AssetCategory(Document):
if selected_key_type not in expected_key_types: if selected_key_type not in expected_key_types:
frappe.throw( frappe.throw(
_( _(
"Row #{}: {} of {} should be {}. Please modify the account or select a different account." "Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account."
).format( ).format(
d.idx, d.idx,
frappe.unscrub(key_to_match), frappe.unscrub(key_to_match),
frappe.bold(selected_account), frappe.bold(selected_account),
frappe.bold(expected_key_types), frappe.bold(" or ".join(expected_key_types)),
), ),
title=_("Invalid Account"), title=_("Invalid Account"),
) )

View File

@ -9,6 +9,7 @@
"field_order": [ "field_order": [
"asset", "asset",
"naming_series", "naming_series",
"company",
"column_break_2", "column_break_2",
"gross_purchase_amount", "gross_purchase_amount",
"opening_accumulated_depreciation", "opening_accumulated_depreciation",
@ -193,12 +194,20 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Depreciate based on shifts", "label": "Depreciate based on shifts",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "asset.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-11-29 00:57:00.461998", "modified": "2024-01-08 16:31:04.533928",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Depreciation Schedule", "name": "Asset Depreciation Schedule",

View File

@ -35,6 +35,7 @@ class AssetDepreciationSchedule(Document):
amended_from: DF.Link | None amended_from: DF.Link | None
asset: DF.Link asset: DF.Link
company: DF.Link | None
daily_prorata_based: DF.Check daily_prorata_based: DF.Check
depreciation_method: DF.Literal[ depreciation_method: DF.Literal[
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual" "", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"
@ -340,10 +341,7 @@ class AssetDepreciationSchedule(Document):
n == 0 n == 0
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation and not self.opening_accumulated_depreciation
and get_updated_rate_of_depreciation_for_wdv_and_dd( and not self.flags.wdv_it_act_applied
asset_doc, value_after_depreciation, row, False
)
== row.rate_of_depreciation
): ):
from_date = add_days( from_date = add_days(
asset_doc.available_for_use_date, -1 asset_doc.available_for_use_date, -1
@ -595,26 +593,17 @@ def get_depreciation_amount(
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
) )
else: else:
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row
)
return get_wdv_or_dd_depr_amount( return get_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value, depreciable_value,
rate_of_depreciation,
fb_row.frequency_of_depreciation,
schedule_idx, schedule_idx,
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
) )
@erpnext.allow_regional
def get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row, show_msg=True
):
return fb_row.rate_of_depreciation
def get_straight_line_or_manual_depr_amount( def get_straight_line_or_manual_depr_amount(
asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations
): ):
@ -750,30 +739,56 @@ def get_asset_shift_factors_map():
return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
@erpnext.allow_regional
def get_wdv_or_dd_depr_amount( def get_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value, depreciable_value,
rate_of_depreciation,
frequency_of_depreciation,
schedule_idx, schedule_idx,
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
): ):
if cint(frequency_of_depreciation) == 12: return get_default_wdv_or_dd_depr_amount(
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) asset,
fb_row,
depreciable_value,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
)
def get_default_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
):
if cint(fb_row.frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
else: else:
if has_wdv_or_dd_non_yearly_pro_rata: if has_wdv_or_dd_non_yearly_pro_rata:
if schedule_idx == 0: if schedule_idx == 0:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1: elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1:
return ( return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) flt(depreciable_value)
* flt(fb_row.frequency_of_depreciation)
* (flt(fb_row.rate_of_depreciation) / 1200)
) )
else: else:
return prev_depreciation_amount return prev_depreciation_amount
else: else:
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0: if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0:
return ( return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) flt(depreciable_value)
* flt(fb_row.frequency_of_depreciation)
* (flt(fb_row.rate_of_depreciation) / 1200)
) )
else: else:
return prev_depreciation_amount return prev_depreciation_amount

View File

@ -94,7 +94,6 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
"fieldname": "daily_prorata_based", "fieldname": "daily_prorata_based",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Depreciate based on daily pro-rata" "label": "Depreciate based on daily pro-rata"
@ -110,7 +109,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-29 00:57:07.579777", "modified": "2023-12-29 08:49:39.876439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Finance Book", "name": "Asset Finance Book",

View File

@ -214,7 +214,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-11-28 13:01:18.403492", "modified": "2024-01-05 15:26:02.320942",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",
@ -238,6 +238,41 @@
"role": "Purchase Manager", "role": "Purchase Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts User",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Stock Manager",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Stock User",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Purchase User",
"share": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",

View File

@ -29,6 +29,8 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
class TestPurchaseOrder(FrappeTestCase): class TestPurchaseOrder(FrappeTestCase):
def test_purchase_order_qty(self): def test_purchase_order_qty(self):
po = create_purchase_order(qty=1, do_not_save=True) po = create_purchase_order(qty=1, do_not_save=True)
# NonNegativeError with qty=-1
po.append( po.append(
"items", "items",
{ {
@ -39,9 +41,15 @@ class TestPurchaseOrder(FrappeTestCase):
) )
self.assertRaises(frappe.NonNegativeError, po.save) self.assertRaises(frappe.NonNegativeError, po.save)
# InvalidQtyError with qty=0
po.items[1].qty = 0 po.items[1].qty = 0
self.assertRaises(InvalidQtyError, po.save) self.assertRaises(InvalidQtyError, po.save)
# No error with qty=1
po.items[1].qty = 1
po.save()
self.assertEqual(po.items[1].qty, 1)
def test_make_purchase_receipt(self): def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True) po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
@ -1108,7 +1116,7 @@ def create_purchase_order(**args):
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"from_warehouse": args.from_warehouse, "from_warehouse": args.from_warehouse,
"qty": args.qty or 10, "qty": args.qty if args.qty is not None else 10,
"rate": args.rate or 500, "rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1), "schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1), "include_exploded_items": args.get("include_exploded_items", 1),

View File

@ -65,6 +65,7 @@ class RequestforQuotation(BuyingController):
def validate(self): def validate(self):
self.validate_duplicate_supplier() self.validate_duplicate_supplier()
self.validate_supplier_list() self.validate_supplier_list()
super(RequestforQuotation, self).validate_qty_is_not_zero()
validate_for_items(self) validate_for_items(self)
super(RequestforQuotation, self).set_qty_as_per_stock_uom() super(RequestforQuotation, self).set_qty_as_per_stock_uom()
self.update_email_id() self.update_email_id()
@ -357,8 +358,8 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=
target_doc.currency = args.currency or get_party_account_currency( target_doc.currency = args.currency or get_party_account_currency(
"Supplier", for_supplier, source.company "Supplier", for_supplier, source.company
) )
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value( target_doc.buying_price_list = args.buying_price_list or frappe.db.get_single_value(
"Buying Settings", None, "buying_price_list" "Buying Settings", "buying_price_list"
) )
set_missing_values(source, target_doc) set_missing_values(source, target_doc)
@ -398,7 +399,7 @@ def create_supplier_quotation(doc):
"currency": doc.get("currency") "currency": doc.get("currency")
or get_party_account_currency("Supplier", doc.get("supplier"), doc.get("company")), or get_party_account_currency("Supplier", doc.get("supplier"), doc.get("company")),
"buying_price_list": doc.get("buying_price_list") "buying_price_list": doc.get("buying_price_list")
or frappe.db.get_value("Buying Settings", None, "buying_price_list"), or frappe.db.get_single_value("Buying Settings", "buying_price_list"),
} }
) )
add_items(sq_doc, doc.get("supplier"), doc.get("items")) add_items(sq_doc, doc.get("supplier"), doc.get("items"))

View File

@ -14,6 +14,7 @@ from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
get_pdf, get_pdf,
make_supplier_quotation_from_rfq, make_supplier_quotation_from_rfq,
) )
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
@ -21,6 +22,16 @@ from erpnext.templates.pages.rfq import check_supplier_has_docname_access
class TestRequestforQuotation(FrappeTestCase): class TestRequestforQuotation(FrappeTestCase):
def test_rfq_qty(self):
rfq = make_request_for_quotation(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
rfq.save()
# No error with qty=1
rfq.items[0].qty = 1
rfq.save()
self.assertEqual(rfq.items[0].qty, 1)
def test_quote_status(self): def test_quote_status(self):
rfq = make_request_for_quotation() rfq = make_request_for_quotation()
@ -161,13 +172,16 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
"description": "_Test Item", "description": "_Test Item",
"uom": args.uom or "_Test UOM", "uom": args.uom or "_Test UOM",
"stock_uom": args.stock_uom or "_Test UOM", "stock_uom": args.stock_uom or "_Test UOM",
"qty": args.qty or 5, "qty": args.qty if args.qty is not None else 5,
"conversion_factor": args.conversion_factor or 1.0, "conversion_factor": args.conversion_factor or 1.0,
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"schedule_date": nowdate(), "schedule_date": nowdate(),
}, },
) )
if not args.do_not_save:
rfq.insert()
if not args.do_not_submit:
rfq.submit() rfq.submit()
return rfq return rfq

View File

@ -5,8 +5,21 @@
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.accounts_controller import InvalidQtyError
class TestPurchaseOrder(FrappeTestCase): class TestPurchaseOrder(FrappeTestCase):
def test_supplier_quotation_qty(self):
sq = frappe.copy_doc(test_records[0])
sq.items[0].qty = 0
with self.assertRaises(InvalidQtyError):
sq.save()
# No error with qty=1
sq.items[0].qty = 1
sq.save()
self.assertEqual(sq.items[0].qty, 1)
def test_make_purchase_order(self): def test_make_purchase_order(self):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order

View File

@ -44,11 +44,6 @@ def update_last_purchase_rate(doc, is_submit) -> None:
def validate_for_items(doc) -> None: def validate_for_items(doc) -> None:
items = [] items = []
for d in doc.get("items"): for d in doc.get("items"):
if not d.qty:
if doc.doctype == "Purchase Receipt" and d.rejected_qty:
continue
frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
set_stock_levels(row=d) # update with latest quantities set_stock_levels(row=d) # update with latest quantities
item = validate_item_and_get_basic_data(row=d) item = validate_item_and_get_basic_data(row=d)
validate_stock_item_warehouse(row=d, item=item) validate_stock_item_warehouse(row=d, item=item)

View File

@ -975,13 +975,15 @@ class AccountsController(TransactionBase):
return flt(args.get(field, 0) / (args.get("conversion_rate") or self.get("conversion_rate", 1))) return flt(args.get(field, 0) / (args.get("conversion_rate") or self.get("conversion_rate", 1)))
def validate_qty_is_not_zero(self): def validate_qty_is_not_zero(self):
if self.doctype == "Purchase Receipt":
return
for item in self.items: for item in self.items:
if self.doctype == "Purchase Receipt" and item.rejected_qty:
continue
if not flt(item.qty): if not flt(item.qty):
frappe.throw( frappe.throw(
msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx), msg=_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
item.idx, frappe.bold(item.item_code)
),
title=_("Invalid Quantity"), title=_("Invalid Quantity"),
exc=InvalidQtyError, exc=InvalidQtyError,
) )
@ -1960,7 +1962,7 @@ class AccountsController(TransactionBase):
self.remove(item) self.remove(item)
def set_payment_schedule(self): def set_payment_schedule(self):
if self.doctype == "Sales Invoice" and self.is_pos: if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
self.payment_terms_template = "" self.payment_terms_template = ""
return return
@ -2143,7 +2145,7 @@ class AccountsController(TransactionBase):
) )
def validate_payment_schedule_amount(self): def validate_payment_schedule_amount(self):
if self.doctype == "Sales Invoice" and self.is_pos: if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
return return
party_account_currency = self.get("party_account_currency") party_account_currency = self.get("party_account_currency")
@ -3097,7 +3099,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
def validate_quantity(child_item, new_data): def validate_quantity(child_item, new_data):
if not flt(new_data.get("qty")): if not flt(new_data.get("qty")):
frappe.throw( frappe.throw(
_("Row # {0}: Quantity for Item {1} cannot be zero").format( _("Row #{0}: Quantity for Item {1} cannot be zero.").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code")) new_data.get("idx"), frappe.bold(new_data.get("item_code"))
), ),
title=_("Invalid Qty"), title=_("Invalid Qty"),

View File

@ -744,11 +744,8 @@ class BuyingController(SubcontractingController):
item_data = frappe.db.get_value( item_data = frappe.db.get_value(
"Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1 "Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1
) )
asset_quantity = row.qty if is_grouped_asset else 1
if is_grouped_asset: purchase_amount = flt(row.valuation_rate) * asset_quantity
purchase_amount = flt(row.base_amount + row.item_tax_amount)
else:
purchase_amount = flt(row.base_rate + row.item_tax_amount)
asset = frappe.get_doc( asset = frappe.get_doc(
{ {
@ -764,7 +761,7 @@ class BuyingController(SubcontractingController):
"calculate_depreciation": 0, "calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount, "purchase_receipt_amount": purchase_amount,
"gross_purchase_amount": purchase_amount, "gross_purchase_amount": purchase_amount,
"asset_quantity": row.qty if is_grouped_asset else 1, "asset_quantity": asset_quantity,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
} }

View File

@ -10,7 +10,7 @@ from frappe.utils import flt, format_datetime, get_datetime
import erpnext import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class StockOverReturnError(frappe.ValidationError): class StockOverReturnError(frappe.ValidationError):
@ -116,7 +116,12 @@ def validate_returned_items(doc):
ref = valid_items.get(d.item_code, frappe._dict()) ref = valid_items.get(d.item_code, frappe._dict())
validate_quantity(doc, d, ref, valid_items, already_returned_items) validate_quantity(doc, d, ref, valid_items, already_returned_items)
if ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") and flt(d.rate) > ref.rate: if (
ref.rate
and flt(d.rate) > ref.rate
and doc.doctype in ("Delivery Note", "Sales Invoice")
and get_valuation_method(ref.item_code) != "Moving Average"
):
frappe.throw( frappe.throw(
_("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format( _("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format(
d.idx, doc.doctype, doc.return_against d.idx, doc.doctype, doc.return_against

View File

@ -295,9 +295,6 @@ class SellingController(StockController):
def get_item_list(self): def get_item_list(self):
il = [] il = []
for d in self.get("items"): for d in self.get("items"):
if d.qty is None:
frappe.throw(_("Row {0}: Qty is mandatory").format(d.idx))
if self.has_product_bundle(d.item_code): if self.has_product_bundle(d.item_code):
for p in self.get("packed_items"): for p in self.get("packed_items"):
if p.parent_detail_docname == d.name and p.parent_item == d.item_code: if p.parent_detail_docname == d.name and p.parent_item == d.item_code:
@ -432,6 +429,9 @@ class SellingController(StockController):
items = self.get("items") + (self.get("packed_items") or []) items = self.get("items") + (self.get("packed_items") or [])
for d in items: for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
continue
if not self.get("return_against") or ( if not self.get("return_against") or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
): ):

View File

@ -131,11 +131,6 @@ status_map = {
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'",
], ],
], ],
"Bank Transaction": [
["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"],
["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"],
["Cancelled", "eval:self.docstatus == 2"],
],
"POS Opening Entry": [ "POS Opening Entry": [
["Draft", None], ["Draft", None],
["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"], ["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],

View File

@ -76,7 +76,7 @@ class Appointment(Document):
self.create_calendar_event() self.create_calendar_event()
else: else:
# Set status to unverified # Set status to unverified
self.status = "Unverified" self.db_set("status", "Unverified")
# Send email to confirm # Send email to confirm
self.send_confirmation_email() self.send_confirmation_email()

View File

@ -129,9 +129,7 @@ class TallyMigration(Document):
self.default_cost_center, self.default_round_off_account = frappe.db.get_value( self.default_cost_center, self.default_round_off_account = frappe.db.get_value(
"Company", self.erpnext_company, ["cost_center", "round_off_account"] "Company", self.erpnext_company, ["cost_center", "round_off_account"]
) )
self.default_warehouse = frappe.db.get_value( self.default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse")
"Stock Settings", "Stock Settings", "default_warehouse"
)
def _process_master_data(self): def _process_master_data(self):
def get_company_name(collection): def get_company_name(collection):

View File

@ -489,6 +489,7 @@ bank_reconciliation_doctypes = [
"Payment Entry", "Payment Entry",
"Journal Entry", "Journal Entry",
"Purchase Invoice", "Purchase Invoice",
"Sales Invoice",
] ]
accounting_dimension_doctypes = [ accounting_dimension_doctypes = [

82664
erpnext/locale/af.po Normal file

File diff suppressed because it is too large Load Diff

82539
erpnext/locale/ar.po Normal file

File diff suppressed because it is too large Load Diff

82887
erpnext/locale/de.po Normal file

File diff suppressed because it is too large Load Diff

82816
erpnext/locale/es.po Normal file

File diff suppressed because it is too large Load Diff

82461
erpnext/locale/fi.po Normal file

File diff suppressed because it is too large Load Diff

82915
erpnext/locale/fr.po Normal file

File diff suppressed because it is too large Load Diff

82588
erpnext/locale/id.po Normal file

File diff suppressed because it is too large Load Diff

82855
erpnext/locale/it.po Normal file

File diff suppressed because it is too large Load Diff

82018
erpnext/locale/main.pot Normal file

File diff suppressed because it is too large Load Diff

82673
erpnext/locale/nl.po Normal file

File diff suppressed because it is too large Load Diff

82620
erpnext/locale/pl.po Normal file

File diff suppressed because it is too large Load Diff

82701
erpnext/locale/pt.po Normal file

File diff suppressed because it is too large Load Diff

82672
erpnext/locale/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

82620
erpnext/locale/ru.po Normal file

File diff suppressed because it is too large Load Diff

82456
erpnext/locale/tr.po Normal file

File diff suppressed because it is too large Load Diff

82508
erpnext/locale/vi.po Normal file

File diff suppressed because it is too large Load Diff

81950
erpnext/locale/zh.po Normal file

File diff suppressed because it is too large Load Diff

81952
erpnext/locale/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -117,7 +117,7 @@ class MaintenanceSchedule(TransactionBase):
self.update_amc_date(serial_nos, d.end_date) self.update_amc_date(serial_nos, d.end_date)
no_email_sp = [] no_email_sp = []
if d.sales_person not in email_map: if d.sales_person and d.sales_person not in email_map:
sp = frappe.get_doc("Sales Person", d.sales_person) sp = frappe.get_doc("Sales Person", d.sales_person)
try: try:
email_map[d.sales_person] = sp.get_email_id() email_map[d.sales_person] = sp.get_email_id()
@ -131,12 +131,11 @@ class MaintenanceSchedule(TransactionBase):
).format(self.owner, "<br>" + "<br>".join(no_email_sp)) ).format(self.owner, "<br>" + "<br>".join(no_email_sp))
) )
scheduled_date = frappe.db.sql( scheduled_date = frappe.db.get_all(
"""select scheduled_date from "Maintenance Schedule Detail",
`tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and {"parent": self.name, "item_code": d.item_code},
parent=%s""", ["scheduled_date"],
(d.sales_person, d.item_code, self.name), as_list=False,
as_dict=1,
) )
for key in scheduled_date: for key in scheduled_date:
@ -232,8 +231,6 @@ class MaintenanceSchedule(TransactionBase):
throw(_("Please select Start Date and End Date for Item {0}").format(d.item_code)) throw(_("Please select Start Date and End Date for Item {0}").format(d.item_code))
elif not d.no_of_visits: elif not d.no_of_visits:
throw(_("Please mention no of visits required")) throw(_("Please mention no of visits required"))
elif not d.sales_person:
throw(_("Please select a Sales Person for item: {0}").format(d.item_name))
if getdate(d.start_date) >= getdate(d.end_date): if getdate(d.start_date) >= getdate(d.end_date):
throw(_("Start date should be less than end date for Item {0}").format(d.item_code)) throw(_("Start date should be less than end date for Item {0}").format(d.item_code))
@ -452,15 +449,23 @@ def get_serial_nos_from_schedule(item_code, schedule=None):
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None): def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
def condition(doc):
if s_id:
return doc.name == s_id
elif item_name:
return doc.item_name == item_name
return True
def update_status_and_detail(source, target, parent): def update_status_and_detail(source, target, parent):
target.maintenance_type = "Scheduled" target.maintenance_type = "Scheduled"
target.maintenance_schedule_detail = s_id
def update_serial(source, target, parent): def update_serial(source, target, parent):
if source.serial_and_batch_bundle: if source.item_reference:
serial_nos = frappe.get_doc( if sbb := frappe.db.get_value(
"Serial and Batch Bundle", source.serial_and_batch_bundle "Maintenance Schedule Item", source.item_reference, "serial_and_batch_bundle"
).get_serial_nos() ):
serial_nos = frappe.get_doc("Serial and Batch Bundle", sbb).get_serial_nos()
if len(serial_nos) == 1: if len(serial_nos) == 1:
target.serial_no = serial_nos[0] target.serial_no = serial_nos[0]
@ -477,10 +482,13 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"validation": {"docstatus": ["=", 1]}, "validation": {"docstatus": ["=", 1]},
"postprocess": update_status_and_detail, "postprocess": update_status_and_detail,
}, },
"Maintenance Schedule Item": { "Maintenance Schedule Detail": {
"doctype": "Maintenance Visit Purpose", "doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name if item_name else True, "condition": condition,
"field_map": {"sales_person": "service_person"}, "field_map": {
"sales_person": "service_person",
"name": "maintenance_schedule_detail",
},
"postprocess": update_serial, "postprocess": update_serial,
}, },
}, },

View File

@ -56,7 +56,8 @@ class MaintenanceVisit(TransactionBase):
frappe.throw(_("Add Items in the Purpose Table"), title=_("Purposes Required")) frappe.throw(_("Add Items in the Purpose Table"), title=_("Purposes Required"))
def validate_maintenance_date(self): def validate_maintenance_date(self):
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail: if self.maintenance_type == "Scheduled":
if self.maintenance_schedule_detail:
item_ref = frappe.db.get_value( item_ref = frappe.db.get_value(
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference" "Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference"
) )
@ -70,6 +71,24 @@ class MaintenanceVisit(TransactionBase):
frappe.throw( frappe.throw(
_("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date)) _("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date))
) )
else:
for purpose in self.purposes:
if purpose.maintenance_schedule_detail:
item_ref = frappe.db.get_value(
"Maintenance Schedule Detail", purpose.maintenance_schedule_detail, "item_reference"
)
if item_ref:
start_date, end_date = frappe.db.get_value(
"Maintenance Schedule Item", item_ref, ["start_date", "end_date"]
)
if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(
self.mntc_date
) > get_datetime(end_date):
frappe.throw(
_("Date must be between {0} and {1}").format(
format_date(start_date), format_date(end_date)
)
)
def validate(self): def validate(self):
self.validate_serial_no() self.validate_serial_no()
@ -82,6 +101,7 @@ class MaintenanceVisit(TransactionBase):
if not cancel: if not cancel:
status = self.completion_status status = self.completion_status
actual_date = self.mntc_date actual_date = self.mntc_date
if self.maintenance_schedule_detail: if self.maintenance_schedule_detail:
frappe.db.set_value( frappe.db.set_value(
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status "Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status
@ -89,6 +109,21 @@ class MaintenanceVisit(TransactionBase):
frappe.db.set_value( frappe.db.set_value(
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date "Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date
) )
else:
for purpose in self.purposes:
if purpose.maintenance_schedule_detail:
frappe.db.set_value(
"Maintenance Schedule Detail",
purpose.maintenance_schedule_detail,
"completion_status",
status,
)
frappe.db.set_value(
"Maintenance Schedule Detail",
purpose.maintenance_schedule_detail,
"actual_date",
actual_date,
)
def update_customer_issue(self, flag): def update_customer_issue(self, flag):
if not self.maintenance_schedule: if not self.maintenance_schedule:

View File

@ -17,7 +17,8 @@
"work_details", "work_details",
"work_done", "work_done",
"prevdoc_doctype", "prevdoc_doctype",
"prevdoc_docname" "prevdoc_docname",
"maintenance_schedule_detail"
], ],
"fields": [ "fields": [
{ {
@ -49,6 +50,8 @@
"options": "Serial No" "options": "Serial No"
}, },
{ {
"fetch_from": "item_code.description",
"fetch_if_empty": 1,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"in_list_view": 1, "in_list_view": 1,
@ -56,7 +59,6 @@
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Small Text", "oldfieldtype": "Small Text",
"print_width": "300px", "print_width": "300px",
"reqd": 1,
"width": "300px" "width": "300px"
}, },
{ {
@ -103,12 +105,19 @@
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break" "fieldtype": "Section Break"
},
{
"fieldname": "maintenance_schedule_detail",
"fieldtype": "Data",
"hidden": 1,
"label": "Maintenance Schedule Detail",
"options": "Maintenance Schedule Detail"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-02-27 11:09:33.114458", "modified": "2024-01-05 21:46:53.239830",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Visit Purpose", "name": "Maintenance Visit Purpose",

View File

@ -14,9 +14,10 @@ class MaintenanceVisitPurpose(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
description: DF.TextEditor description: DF.TextEditor | None
item_code: DF.Link | None item_code: DF.Link | None
item_name: DF.Data | None item_name: DF.Data | None
maintenance_schedule_detail: DF.Data | None
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data

View File

@ -12,12 +12,13 @@
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"last_synced_on": "2020-07-21 16:57:09.767009", "last_synced_on": "2020-07-21 16:57:09.767009",
"modified": "2020-07-21 16:57:55.719802", "modified": "2024-01-10 12:21:25.134075",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Completed Operation", "name": "Completed Operation",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"parent_document_type": "Work Order",
"time_interval": "Quarterly", "time_interval": "Quarterly",
"timeseries": 1, "timeseries": 1,
"timespan": "Last Year", "timespan": "Last Year",

View File

@ -27,6 +27,28 @@ test_dependencies = ["Item", "Quality Inspection Template"]
class TestBOM(FrappeTestCase): class TestBOM(FrappeTestCase):
@timeout
def test_bom_qty(self):
from erpnext.stock.doctype.item.test_item import make_item
# No error.
bom = frappe.new_doc("BOM")
item = make_item(properties={"is_stock_item": 1})
bom.item = fg_item.item_code
bom.quantity = 1
bom.append(
"items",
{
"item_code": bom_item.item_code,
"qty": 0,
"uom": bom_item.stock_uom,
"stock_uom": bom_item.stock_uom,
"rate": 100.0,
},
)
bom.save()
self.assertEqual(bom.items[0].qty, 0)
@timeout @timeout
def test_get_items(self): def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict

View File

@ -86,10 +86,12 @@ def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
if new_bom == d.parent: if new_bom == d.parent:
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
if d.parent not in tuple(bom_list):
bom_list.append(d.parent) bom_list.append(d.parent)
get_ancestor_boms(d.parent, bom_list) get_ancestor_boms(d.parent, bom_list)
return list(set(bom_list)) return bom_list
def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:

View File

@ -57,6 +57,68 @@ class TestBOMUpdateLog(FrappeTestCase):
log.reload() log.reload()
self.assertEqual(log.status, "Completed") self.assertEqual(log.status, "Completed")
def test_bom_replace_for_root_bom(self):
"""
- B-Item A (Root Item)
- B-Item B
- B-Item C
- B-Item D
- B-Item E
- B-Item F
Create New BOM for B-Item E with B-Item G and replace it in the above BOM.
"""
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.item.test_item import make_item
items = ["B-Item A", "B-Item B", "B-Item C", "B-Item D", "B-Item E", "B-Item F", "B-Item G"]
for item_code in items:
if not frappe.db.exists("Item", item_code):
make_item(item_code)
for item_code in items:
remove_bom(item_code)
bom_tree = {
"B-Item A": {"B-Item B": {"B-Item C": {}}, "B-Item D": {"B-Item E": {"B-Item F": {}}}}
}
root_bom = create_nested_bom(bom_tree, prefix="")
exploded_items = frappe.get_all(
"BOM Explosion Item", filters={"parent": root_bom.name}, fields=["item_code"]
)
exploded_items = [item.item_code for item in exploded_items]
expected_exploded_items = ["B-Item C", "B-Item F"]
self.assertEqual(sorted(exploded_items), sorted(expected_exploded_items))
old_bom = frappe.db.get_value("BOM", {"item": "B-Item E"}, "name")
bom_tree = {"B-Item E": {"B-Item G": {}}}
new_bom = create_nested_bom(bom_tree, prefix="")
enqueue_replace_bom(boms=frappe._dict(current_bom=old_bom, new_bom=new_bom.name))
exploded_items = frappe.get_all(
"BOM Explosion Item", filters={"parent": root_bom.name}, fields=["item_code"]
)
exploded_items = [item.item_code for item in exploded_items]
expected_exploded_items = ["B-Item C", "B-Item G"]
self.assertEqual(sorted(exploded_items), sorted(expected_exploded_items))
def remove_bom(item_code):
boms = frappe.get_all("BOM", fields=["docstatus", "name"], filters={"item": item_code})
for row in boms:
if row.docstatus == 1:
frappe.get_doc("BOM", row.name).cancel()
frappe.delete_doc("BOM", row.name)
def update_cost_in_all_boms_in_test(): def update_cost_in_all_boms_in_test():
""" """

View File

@ -173,7 +173,7 @@ frappe.ui.form.on('Production Plan', {
method: "set_status", method: "set_status",
freeze: true, freeze: true,
doc: frm.doc, doc: frm.doc,
args: {close : close}, args: {close : close, update_bin: true},
callback: function() { callback: function() {
frm.reload_doc(); frm.reload_doc();
} }

View File

@ -579,7 +579,7 @@ class ProductionPlan(Document):
frappe.delete_doc("Work Order", d.name) frappe.delete_doc("Work Order", d.name)
@frappe.whitelist() @frappe.whitelist()
def set_status(self, close=None): def set_status(self, close=None, update_bin=False):
self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus) self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus)
if close: if close:
@ -599,7 +599,7 @@ class ProductionPlan(Document):
if close is not None: if close is not None:
self.db_set("status", self.status) self.db_set("status", self.status)
if self.docstatus == 1 and self.status != "Completed": if update_bin and self.docstatus == 1 and self.status != "Completed":
self.update_bin_qty() self.update_bin_qty()
def update_ordered_status(self): def update_ordered_status(self):
@ -646,6 +646,10 @@ class ProductionPlan(Document):
"project": self.project, "project": self.project,
} }
key = (d.item_code, d.sales_order, d.warehouse)
if not d.sales_order:
key = (d.name, d.item_code, d.warehouse)
if not item_details["project"] and d.sales_order: if not item_details["project"] and d.sales_order:
item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project") item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
@ -654,12 +658,9 @@ class ProductionPlan(Document):
item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
else: else:
item_details.update( item_details.update(
{ {"qty": flt(item_dict.get(key, {}).get("qty")) + (flt(d.planned_qty) - flt(d.ordered_qty))}
"qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse), {}).get("qty"))
+ (flt(d.planned_qty) - flt(d.ordered_qty))
}
) )
item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details item_dict[key] = item_details
return item_dict return item_dict

View File

@ -672,7 +672,7 @@ class TestProductionPlan(FrappeTestCase):
items_data = pln.get_production_items() items_data = pln.get_production_items()
# Update qty # Update qty
items_data[(item, None, None)]["qty"] = qty items_data[(pln.po_items[0].name, item, None)]["qty"] = qty
# Create and Submit Work Order for each item in items_data # Create and Submit Work Order for each item in items_data
for key, item in items_data.items(): for key, item in items_data.items():
@ -1486,14 +1486,14 @@ class TestProductionPlan(FrappeTestCase):
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln.reload() pln.reload()
pln.set_status(close=True) pln.set_status(close=True, update_bin=True)
bin_name = get_or_make_bin(rm_item, rm_warehouse) bin_name = get_or_make_bin(rm_item, rm_warehouse)
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertAlmostEqual(after_qty, before_qty - 10) self.assertAlmostEqual(after_qty, before_qty - 10)
pln.reload() pln.reload()
pln.set_status(close=False) pln.set_status(close=False, update_bin=True)
bin_name = get_or_make_bin(rm_item, rm_warehouse) bin_name = get_or_make_bin(rm_item, rm_warehouse)
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
@ -1522,6 +1522,45 @@ class TestProductionPlan(FrappeTestCase):
for d in mr_items: for d in mr_items:
self.assertEqual(d.get("quantity"), 1000.0) self.assertEqual(d.get("quantity"), 1000.0)
def test_fg_item_quantity(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_or_make_bin
fg_item = make_item(properties={"is_stock_item": 1}).name
rm_item = make_item(properties={"is_stock_item": 1}).name
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1)
pln.append(
"po_items",
{
"item_code": rm_item,
"planned_qty": 20,
"bom_no": pln.po_items[0].bom_no,
"warehouse": pln.po_items[0].warehouse,
"planned_start_date": add_to_date(nowdate(), days=1),
},
)
pln.submit()
wo_qty = {}
for row in pln.po_items:
wo_qty[row.name] = row.planned_qty
pln.make_work_order()
work_orders = frappe.get_all(
"Work Order",
fields=["qty", "production_plan_item as name"],
filters={"production_plan": pln.name},
)
self.assertEqual(len(work_orders), 2)
for row in work_orders:
self.assertEqual(row.qty, wo_qty[row.name])
def create_production_plan(**args): def create_production_plan(**args):
""" """

View File

@ -156,7 +156,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da
if not frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"): if not frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"):
check_workstation_for_holiday(workstation, from_datetime, to_datetime) check_workstation_for_holiday(workstation, from_datetime, to_datetime)
if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")): if not cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime")):
is_within_operating_hours(workstation, operation, from_datetime, to_datetime) is_within_operating_hours(workstation, operation, from_datetime, to_datetime)

View File

@ -45,7 +45,7 @@ def execute():
message=json.dumps(purchase_invoices + sales_invoices, indent=2), message=json.dumps(purchase_invoices + sales_invoices, indent=2),
) )
acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto") acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
if acc_frozen_upto: if acc_frozen_upto:
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)

View File

@ -2,12 +2,7 @@ import frappe
def execute(): def execute():
settings = frappe.db.get_value( settings = frappe.db.get_singles_dict("Selling Settings", cast=True)
"Selling Settings",
"Selling Settings",
["campaign_naming_by", "close_opportunity_after_days", "default_valid_till"],
as_dict=True,
)
frappe.reload_doc("crm", "doctype", "crm_settings") frappe.reload_doc("crm", "doctype", "crm_settings")
if settings: if settings:

View File

@ -131,6 +131,7 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"bold": 1,
"fieldname": "expected_start_date", "fieldname": "expected_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Expected Start Date", "label": "Expected Start Date",
@ -453,7 +454,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"max_attachments": 4, "max_attachments": 4,
"modified": "2023-08-28 22:27:28.370849", "modified": "2024-01-08 16:01:34.598258",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Project", "name": "Project",

View File

@ -19,6 +19,62 @@ from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
class Project(Document): class Project(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.projects.doctype.project_user.project_user import ProjectUser
actual_end_date: DF.Date | None
actual_start_date: DF.Date | None
actual_time: DF.Float
collect_progress: DF.Check
company: DF.Link
copied_from: DF.Data | None
cost_center: DF.Link | None
customer: DF.Link | None
daily_time_to_send: DF.Time | None
day_to_send: DF.Literal[
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
]
department: DF.Link | None
estimated_costing: DF.Currency
expected_end_date: DF.Date | None
expected_start_date: DF.Date | None
first_email: DF.Time | None
frequency: DF.Literal["Hourly", "Twice Daily", "Daily", "Weekly"]
from_time: DF.Time | None
gross_margin: DF.Currency
holiday_list: DF.Link | None
is_active: DF.Literal["Yes", "No"]
message: DF.Text | None
naming_series: DF.Literal["PROJ-.####"]
notes: DF.TextEditor | None
per_gross_margin: DF.Percent
percent_complete: DF.Percent
percent_complete_method: DF.Literal["Manual", "Task Completion", "Task Progress", "Task Weight"]
priority: DF.Literal["Medium", "Low", "High"]
project_name: DF.Data
project_template: DF.Link | None
project_type: DF.Link | None
sales_order: DF.Link | None
second_email: DF.Time | None
status: DF.Literal["Open", "Completed", "Cancelled"]
to_time: DF.Time | None
total_billable_amount: DF.Currency
total_billed_amount: DF.Currency
total_consumed_material_cost: DF.Currency
total_costing_amount: DF.Currency
total_purchase_cost: DF.Currency
total_sales_amount: DF.Currency
users: DF.Table[ProjectUser]
weekly_time_to_send: DF.Time | None
# end: auto-generated types
def onload(self): def onload(self):
self.set_onload( self.set_onload(
"activity_summary", "activity_summary",
@ -314,17 +370,15 @@ def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
def get_project_list( def get_project_list(
doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"
): ):
user = frappe.session.user
customers, suppliers = get_customers_suppliers("Project", frappe.session.user) customers, suppliers = get_customers_suppliers("Project", frappe.session.user)
ignore_permissions = False ignore_permissions = False
if is_website_user(): if is_website_user() and frappe.session.user != "Guest":
if not filters: if not filters:
filters = [] filters = []
if customers: if customers:
filters.append([doctype, "customer", "in", customers]) filters.append([doctype, "customer", "in", customers])
ignore_permissions = True ignore_permissions = True
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)

View File

@ -153,6 +153,7 @@
"label": "Timeline" "label": "Timeline"
}, },
{ {
"bold": 1,
"fieldname": "exp_start_date", "fieldname": "exp_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Expected Start Date", "label": "Expected Start Date",
@ -398,7 +399,7 @@
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"max_attachments": 5, "max_attachments": 5,
"modified": "2023-11-20 11:42:41.884069", "modified": "2024-01-08 16:00:41.296203",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Task", "name": "Task",

View File

@ -790,7 +790,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (me.frm.doc.price_list_currency == company_currency) { if (me.frm.doc.price_list_currency == company_currency) {
me.frm.set_value('plc_conversion_rate', 1.0); me.frm.set_value('plc_conversion_rate', 1.0);
} }
if (company_doc.default_letter_head) { if (company_doc && company_doc.default_letter_head) {
if(me.frm.fields_dict.letter_head) { if(me.frm.fields_dict.letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head); me.frm.set_value("letter_head", company_doc.default_letter_head);
} }

View File

@ -16,6 +16,8 @@ erpnext.accounts.dimensions = {
}, },
callback: function(r) { callback: function(r) {
me.accounting_dimensions = r.message[0]; me.accounting_dimensions = r.message[0];
// Ignoring "Project" as it is already handled specifically in Sales Order and Delivery Note
me.accounting_dimensions = me.accounting_dimensions.filter(x=>{return x.document_type != "Project"});
me.default_dimensions = r.message[1]; me.default_dimensions = r.message[1];
me.setup_filters(frm, doctype); me.setup_filters(frm, doctype);
} }

View File

@ -370,6 +370,7 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_
) )
# sales team # sales team
if not target.get("sales_team"):
for d in customer.get("sales_team") or []: for d in customer.get("sales_team") or []:
target.append( target.append(
"sales_team", "sales_team",

View File

@ -5,10 +5,22 @@ import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, add_months, flt, getdate, nowdate from frappe.utils import add_days, add_months, flt, getdate, nowdate
from erpnext.controllers.accounts_controller import InvalidQtyError
test_dependencies = ["Product Bundle"] test_dependencies = ["Product Bundle"]
class TestQuotation(FrappeTestCase): class TestQuotation(FrappeTestCase):
def test_quotation_qty(self):
qo = make_quotation(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
qo.save()
# No error with qty=1
qo.items[0].qty = 1
qo.save()
self.assertEqual(qo.items[0].qty, 1)
def test_make_quotation_without_terms(self): def test_make_quotation_without_terms(self):
quotation = make_quotation(do_not_save=1) quotation = make_quotation(do_not_save=1)
self.assertFalse(quotation.get("payment_schedule")) self.assertFalse(quotation.get("payment_schedule"))
@ -629,7 +641,7 @@ def make_quotation(**args):
{ {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse, "warehouse": args.warehouse,
"qty": args.qty or 10, "qty": args.qty if args.qty is not None else 10,
"uom": args.uom or None, "uom": args.uom or None,
"rate": args.rate or 100, "rate": args.rate or 100,
}, },

View File

@ -582,17 +582,17 @@ class SalesOrder(SellingController):
def set_indicator(self): def set_indicator(self):
"""Set indicator for portal""" """Set indicator for portal"""
if self.per_billed < 100 and self.per_delivered < 100: self.indicator_color = {
self.indicator_color = "orange" "Draft": "red",
self.indicator_title = _("Not Paid and Not Delivered") "On Hold": "orange",
"To Deliver and Bill": "orange",
"To Bill": "orange",
"To Deliver": "orange",
"Completed": "green",
"Cancelled": "red",
}.get(self.status, "blue")
elif self.per_billed == 100 and self.per_delivered < 100: self.indicator_title = _(self.status)
self.indicator_color = "orange"
self.indicator_title = _("Paid and Not Delivered")
else:
self.indicator_color = "green"
self.indicator_title = _("Paid")
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):

View File

@ -9,7 +9,7 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today from frappe.utils import add_days, flt, getdate, nowdate, today
from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
make_maintenance_schedule, make_maintenance_schedule,
) )
@ -35,8 +35,8 @@ class TestSalesOrder(FrappeTestCase):
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.unlink_setting = int( cls.unlink_setting = int(
frappe.db.get_value( frappe.db.get_single_value(
"Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order" "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order"
) )
) )
@ -80,6 +80,29 @@ class TestSalesOrder(FrappeTestCase):
) )
update_child_qty_rate("Sales Order", trans_item, so.name) update_child_qty_rate("Sales Order", trans_item, so.name)
def test_sales_order_qty(self):
so = make_sales_order(qty=1, do_not_save=True)
# NonNegativeError with qty=-1
so.append(
"items",
{
"item_code": "_Test Item",
"qty": -1,
"rate": 10,
},
)
self.assertRaises(frappe.NonNegativeError, so.save)
# InvalidQtyError with qty=0
so.items[1].qty = 0
self.assertRaises(InvalidQtyError, so.save)
# No error with qty=1
so.items[1].qty = 1
so.save()
self.assertEqual(so.items[0].qty, 1)
def test_make_material_request(self): def test_make_material_request(self):
so = make_sales_order(do_not_submit=True) so = make_sales_order(do_not_submit=True)
@ -2015,7 +2038,7 @@ def make_sales_order(**args):
{ {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse, "warehouse": args.warehouse,
"qty": args.qty or 10, "qty": args.qty if args.qty is not None else 10,
"uom": args.uom or None, "uom": args.uom or None,
"price_list_rate": args.price_list_rate or None, "price_list_rate": args.price_list_rate or None,
"discount_percentage": args.discount_percentage or None, "discount_percentage": args.discount_percentage or None,

View File

@ -13,7 +13,7 @@ def execute(filters=None):
if not filters: if not filters:
filters = {} filters = {}
# Check if customer id is according to naming series or customer name # Check if customer id is according to naming series or customer name
customer_naming_type = frappe.db.get_value("Selling Settings", None, "cust_master_name") customer_naming_type = frappe.db.get_single_value("Selling Settings", "cust_master_name")
columns = get_columns(customer_naming_type) columns = get_columns(customer_naming_type)
data = [] data = []

View File

@ -87,7 +87,7 @@ class Employee(NestedSet):
def update_user_permissions(self): def update_user_permissions(self):
if not self.create_user_permission: if not self.create_user_permission:
return return
if not has_permission("User Permission", ptype="write", raise_exception=False): if not has_permission("User Permission", ptype="write", print_logs=False):
return return
employee_user_permission_exists = frappe.db.exists( employee_user_permission_exists = frappe.db.exists(
@ -258,7 +258,7 @@ def validate_employee_role(doc, method=None, ignore_emp_check=False):
def update_user_permissions(doc, method): def update_user_permissions(doc, method):
# called via User hook # called via User hook
if "Employee" in [d.role for d in doc.get("roles")]: if "Employee" in [d.role for d in doc.get("roles")]:
if not has_permission("User Permission", ptype="write", raise_exception=False): if not has_permission("User Permission", ptype="write", print_logs=False):
return return
employee = frappe.get_doc("Employee", {"user_id": doc.name}) employee = frappe.get_doc("Employee", {"user_id": doc.name})
employee.update_user_permissions() employee.update_user_permissions()

View File

@ -9,8 +9,6 @@ from frappe.utils import cint
def boot_session(bootinfo): def boot_session(bootinfo):
"""boot session - send website info if guest""" """boot session - send website info if guest"""
bootinfo.custom_css = frappe.db.get_value("Style Settings", None, "custom_css") or ""
if frappe.session["user"] != "Guest": if frappe.session["user"] != "Guest":
update_page_info(bootinfo) update_page_info(bootinfo)

View File

@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
& ( & (
(table.from_date.between(self.from_date, self.to_date)) (table.from_date.between(self.from_date, self.to_date))
| (table.to_date.between(self.from_date, self.to_date)) | (table.to_date.between(self.from_date, self.to_date))
| (table.from_date >= self.from_date and table.to_date <= self.to_date) | (table.from_date >= self.from_date and table.to_date >= self.to_date)
) )
) )
) )

View File

@ -10,6 +10,7 @@ from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.test_sales_order import ( from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms, automatically_fetch_payment_terms,
@ -42,6 +43,16 @@ from erpnext.stock.stock_ledger import get_previous_sle
class TestDeliveryNote(FrappeTestCase): class TestDeliveryNote(FrappeTestCase):
def test_delivery_note_qty(self):
dn = create_delivery_note(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
dn.save()
# No error with qty=1
dn.items[0].qty = 1
dn.save()
self.assertEqual(dn.items[0].qty, 1)
def test_over_billing_against_dn(self): def test_over_billing_against_dn(self):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
@ -1518,6 +1529,25 @@ class TestDeliveryNote(FrappeTestCase):
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0
) )
def test_internal_transfer_for_non_stock_item(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
item = make_item(properties={"is_stock_item": 0}).name
warehouse = "_Test Warehouse - _TC"
target = "Stores - _TC"
company = "_Test Company"
customer = create_internal_customer(represents_company=company)
rate = 100
so = make_sales_order(item_code=item, qty=1, rate=rate, customer=customer, warehouse=warehouse)
dn = make_delivery_note(so.name)
dn.items[0].target_warehouse = target
dn.save().submit()
self.assertEqual(so.items[0].rate, rate)
self.assertEqual(dn.items[0].rate, so.items[0].rate)
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")
@ -1539,7 +1569,7 @@ def create_delivery_note(**args):
if dn.is_return: if dn.is_return:
type_of_transaction = "Inward" type_of_transaction = "Inward"
qty = args.get("qty") or 1 qty = args.qty if args.get("qty") is not None else 1
qty *= -1 if type_of_transaction == "Outward" else 1 qty *= -1 if type_of_transaction == "Outward" else 1
batches = {} batches = {}
if args.get("batch_no"): if args.get("batch_no"):
@ -1567,7 +1597,7 @@ def create_delivery_note(**args):
{ {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 1, "qty": args.qty if args.get("qty") is not None else 1,
"rate": args.rate if args.get("rate") is not None else 100, "rate": args.rate if args.get("rate") is not None else 100,
"conversion_factor": 1.0, "conversion_factor": 1.0,
"serial_and_batch_bundle": bundle_id, "serial_and_batch_bundle": bundle_id,

View File

@ -202,6 +202,7 @@
"label": "Allow Alternative Item" "label": "Allow Alternative Item"
}, },
{ {
"allow_in_quick_entry": 1,
"bold": 1, "bold": 1,
"default": "1", "default": "1",
"depends_on": "eval:!doc.is_fixed_asset", "depends_on": "eval:!doc.is_fixed_asset",
@ -239,6 +240,7 @@
"label": "Standard Selling Rate" "label": "Standard Selling Rate"
}, },
{ {
"allow_in_quick_entry": 1,
"default": "0", "default": "0",
"fieldname": "is_fixed_asset", "fieldname": "is_fixed_asset",
"fieldtype": "Check", "fieldtype": "Check",
@ -246,6 +248,7 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"allow_in_quick_entry": 1,
"depends_on": "is_fixed_asset", "depends_on": "is_fixed_asset",
"fieldname": "asset_category", "fieldname": "asset_category",
"fieldtype": "Link", "fieldtype": "Link",
@ -888,7 +891,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-09-18 15:41:32.688051", "modified": "2024-01-08 18:09:30.225085",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -635,8 +635,8 @@ class Item(Document):
def recalculate_bin_qty(self, new_name): def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock from erpnext.stock.stock_balance import repost_stock
existing_allow_negative_stock = frappe.db.get_value( existing_allow_negative_stock = frappe.db.get_single_value(
"Stock Settings", None, "allow_negative_stock" "Stock Settings", "allow_negative_stock"
) )
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)

View File

@ -194,6 +194,7 @@ class LandedCostVoucher(Document):
for d in self.get("purchase_receipts"): for d in self.get("purchase_receipts"):
doc = frappe.get_doc(d.receipt_document_type, d.receipt_document) doc = frappe.get_doc(d.receipt_document_type, d.receipt_document)
# check if there are {qty} assets created and linked to this receipt document # check if there are {qty} assets created and linked to this receipt document
if self.docstatus != 2:
self.validate_asset_qty_and_status(d.receipt_document_type, doc) self.validate_asset_qty_and_status(d.receipt_document_type, doc)
# set landed cost voucher amount in pr item # set landed cost voucher amount in pr item
@ -239,20 +240,20 @@ class LandedCostVoucher(Document):
}, },
fields=["name", "docstatus"], fields=["name", "docstatus"],
) )
if not docs or len(docs) != item.qty: if not docs or len(docs) < item.qty:
frappe.throw( frappe.throw(
_( _(
"There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document." "There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document."
).format(item.receipt_document, item.qty) ).format(len(docs), item.receipt_document, item.qty)
) )
if docs: if docs:
for d in docs: for d in docs:
if d.docstatus == 1: if d.docstatus == 1:
frappe.throw( frappe.throw(
_( _(
"{2} <b>{0}</b> has submitted Assets. Remove Item <b>{1}</b> from table to continue." "{0} <b>{1}</b> has submitted Assets. Remove Item <b>{2}</b> from table to continue."
).format( ).format(
item.receipt_document, item.item_code, item.receipt_document_type item.receipt_document_type, item.receipt_document, item.item_code
) )
) )

View File

@ -9,6 +9,7 @@ import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, today from frappe.utils import flt, today
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.material_request.material_request import ( from erpnext.stock.doctype.material_request.material_request import (
make_in_transit_stock_entry, make_in_transit_stock_entry,
@ -20,6 +21,17 @@ from erpnext.stock.doctype.material_request.material_request import (
class TestMaterialRequest(FrappeTestCase): class TestMaterialRequest(FrappeTestCase):
def test_material_request_qty(self):
mr = frappe.copy_doc(test_records[0])
mr.items[0].qty = 0
with self.assertRaises(InvalidQtyError):
mr.insert()
# No error with qty=1
mr.items[0].qty = 1
mr.save()
self.assertEqual(mr.items[0].qty, 1)
def test_make_purchase_order(self): def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert() mr = frappe.copy_doc(test_records[0]).insert()

View File

@ -39,11 +39,11 @@ class PriceList(Document):
def set_default_if_missing(self): def set_default_if_missing(self):
if cint(self.selling): if cint(self.selling):
if not frappe.db.get_value("Selling Settings", None, "selling_price_list"): if not frappe.db.get_single_value("Selling Settings", "selling_price_list"):
frappe.set_value("Selling Settings", "Selling Settings", "selling_price_list", self.name) frappe.set_value("Selling Settings", "Selling Settings", "selling_price_list", self.name)
elif cint(self.buying): elif cint(self.buying):
if not frappe.db.get_value("Buying Settings", None, "buying_price_list"): if not frappe.db.get_single_value("Buying Settings", "buying_price_list"):
frappe.set_value("Buying Settings", "Buying Settings", "buying_price_list", self.name) frappe.set_value("Buying Settings", "Buying Settings", "buying_price_list", self.name)
def update_item_price(self): def update_item_price(self):

View File

@ -13,6 +13,7 @@ from pypika import functions as fn
import erpnext import erpnext
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.buying_controller import BuyingController from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
@ -302,7 +303,7 @@ class PurchaseReceipt(BuyingController):
) )
def po_required(self): def po_required(self):
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes":
for d in self.get("items"): for d in self.get("items"):
if not d.purchase_order: if not d.purchase_order:
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
@ -674,14 +675,15 @@ class PurchaseReceipt(BuyingController):
landed_cost_entries = get_item_account_wise_additional_cost(self.name) landed_cost_entries = get_item_account_wise_additional_cost(self.name)
if d.is_fixed_asset: if d.is_fixed_asset:
account_type = ( if is_cwip_accounting_enabled(d.asset_category):
"capital_work_in_progress_account"
if is_cwip_accounting_enabled(d.asset_category)
else "fixed_asset_account"
)
stock_asset_account_name = get_asset_account( stock_asset_account_name = get_asset_account(
account_type, asset_category=d.asset_category, company=self.company "capital_work_in_progress_account",
asset_category=d.asset_category,
company=self.company,
)
else:
stock_asset_account_name = get_asset_category_account(
"fixed_asset_account", asset_category=d.asset_category, company=self.company
) )
stock_value_diff = ( stock_value_diff = (
@ -719,7 +721,7 @@ class PurchaseReceipt(BuyingController):
): ):
warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse) warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
if d.is_fixed_asset: if d.is_fixed_asset and d.landed_cost_voucher_amount:
self.update_assets(d, d.valuation_rate) self.update_assets(d, d.valuation_rate)
if warehouse_with_no_account: if warehouse_with_no_account:
@ -852,11 +854,14 @@ class PurchaseReceipt(BuyingController):
) )
for asset in assets: for asset in assets:
purchase_amount = flt(valuation_rate) * asset.asset_quantity
frappe.db.set_value( frappe.db.set_value(
"Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity "Asset",
) asset.name,
frappe.db.set_value( {
"Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity "gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
},
) )
def update_status(self, status): def update_status(self, status):
@ -1228,6 +1233,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
"field_map": { "field_map": {
"name": "pr_detail", "name": "pr_detail",
"parent": "purchase_receipt", "parent": "purchase_receipt",
"qty": "received_qty",
"purchase_order_item": "po_detail", "purchase_order_item": "po_detail",
"purchase_order": "purchase_order", "purchase_order": "purchase_order",
"is_fixed_asset": "is_fixed_asset", "is_fixed_asset": "is_fixed_asset",

View File

@ -9,6 +9,7 @@ from pypika import functions as fn
import erpnext import erpnext
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
@ -30,6 +31,23 @@ class TestPurchaseReceipt(FrappeTestCase):
def setUp(self): def setUp(self):
frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1) frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1)
def test_purchase_receipt_qty(self):
pr = make_purchase_receipt(qty=0, rejected_qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
pr.save()
# No error with qty=1
pr.items[0].qty = 1
pr.save()
self.assertEqual(pr.items[0].qty, 1)
# No error with rejected_qty=1
pr.items[0].rejected_warehouse = "_Test Rejected Warehouse - _TC"
pr.items[0].rejected_qty = 1
pr.items[0].qty = 0
pr.save()
self.assertEqual(pr.items[0].rejected_qty, 1)
def test_purchase_receipt_received_qty(self): def test_purchase_receipt_received_qty(self):
""" """
1. Test if received qty is validated against accepted + rejected 1. Test if received qty is validated against accepted + rejected
@ -2349,7 +2367,8 @@ def make_purchase_receipt(**args):
pr.is_return = args.is_return pr.is_return = args.is_return
pr.return_against = args.return_against pr.return_against = args.return_against
pr.apply_putaway_rule = args.apply_putaway_rule pr.apply_putaway_rule = args.apply_putaway_rule
qty = args.qty or 5
qty = args.qty if args.qty is not None else 5
rejected_qty = args.rejected_qty or 0 rejected_qty = args.rejected_qty or 0
received_qty = args.received_qty or flt(rejected_qty) + flt(qty) received_qty = args.received_qty or flt(rejected_qty) + flt(qty)

View File

@ -111,6 +111,9 @@ class QualityInspection(Document):
def on_cancel(self): def on_cancel(self):
self.update_qc_reference() self.update_qc_reference()
def on_trash(self):
self.update_qc_reference()
def validate_readings_status_mandatory(self): def validate_readings_status_mandatory(self):
for reading in self.readings: for reading in self.readings:
if not reading.status: if not reading.status:

View File

@ -250,6 +250,33 @@ class TestQualityInspection(FrappeTestCase):
qa.delete() qa.delete()
dn.delete() dn.delete()
def test_delete_quality_inspection_linked_with_stock_entry(self):
item_code = create_item("_Test Cicuular Dependecy Item with QA").name
se = make_stock_entry(
item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, do_not_submit=True
)
se.inspection_required = 1
se.save()
qa = create_quality_inspection(
item_code=item_code, reference_type="Stock Entry", reference_name=se.name, do_not_submit=True
)
se.reload()
se.items[0].quality_inspection = qa.name
se.save()
qa.delete()
se.reload()
qc = se.items[0].quality_inspection
self.assertFalse(qc)
se.delete()
def create_quality_inspection(**args): def create_quality_inspection(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -140,12 +140,7 @@ class RepostItemValuation(Document):
return query[0][0] if query else None return query[0][0] if query else None
def validate_accounts_freeze(self): def validate_accounts_freeze(self):
acc_settings = frappe.db.get_value( acc_settings = frappe.get_cached_doc("Accounts Settings")
"Accounts Settings",
"Accounts Settings",
["acc_frozen_upto", "frozen_accounts_modifier"],
as_dict=1,
)
if not acc_settings.acc_frozen_upto: if not acc_settings.acc_frozen_upto:
return return
if getdate(self.posting_date) <= getdate(acc_settings.acc_frozen_upto): if getdate(self.posting_date) <= getdate(acc_settings.acc_frozen_upto):

View File

@ -1004,13 +1004,17 @@ def make_serial_nos(item_code, serial_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1) item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")] serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
existing_serial_nos = frappe.get_all("Serial No", filters={"name": ("in", serial_nos)})
existing_serial_nos = [d.get("name") for d in existing_serial_nos if d.get("name")]
serial_nos = list(set(serial_nos) - set(existing_serial_nos))
if not serial_nos:
return
serial_nos_details = [] serial_nos_details = []
user = frappe.session.user user = frappe.session.user
for serial_no in serial_nos: for serial_no in serial_nos:
if frappe.db.exists("Serial No", serial_no):
continue
serial_nos_details.append( serial_nos_details.append(
( (
serial_no, serial_no,
@ -1046,9 +1050,16 @@ def make_serial_nos(item_code, serial_nos):
def make_batch_nos(item_code, batch_nos): def make_batch_nos(item_code, batch_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1) item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
batch_nos = [d.get("batch_no") for d in batch_nos if d.get("batch_no")] batch_nos = [d.get("batch_no") for d in batch_nos if d.get("batch_no")]
existing_batches = frappe.get_all("Batch", filters={"name": ("in", batch_nos)})
existing_batches = [d.get("name") for d in existing_batches if d.get("name")]
batch_nos = list(set(batch_nos) - set(existing_batches))
if not batch_nos:
return
batch_nos_details = [] batch_nos_details = []
user = frappe.session.user user = frappe.session.user
for batch_no in batch_nos: for batch_no in batch_nos:

Some files were not shown because too many files have changed in this diff Show More