Merge branch 'develop' into remove-je-list-indicators
This commit is contained in:
commit
e69f9ddf8b
@ -91,8 +91,8 @@ class Account(NestedSet):
|
||||
super(Account, self).on_update()
|
||||
|
||||
def onload(self):
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
|
||||
frozen_accounts_modifier = frappe.db.get_single_value(
|
||||
"Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
|
||||
self.set_onload("can_freeze_account", True)
|
||||
|
@ -77,7 +77,7 @@ frappe.treeview_settings["Account"] = {
|
||||
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
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);
|
||||
|
||||
if (account.balance!==undefined) {
|
||||
|
@ -231,6 +231,8 @@ def build_account_tree(tree, parent, all_accounts):
|
||||
tree[child.account_name]["account_type"] = child.account_type
|
||||
if 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:
|
||||
tree[child.account_name]["root_type"] = child.root_type
|
||||
|
||||
|
@ -444,6 +444,10 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
vouchers = json.loads(vouchers)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
transaction.add_payment_entries(vouchers)
|
||||
transaction.validate_duplicate_references()
|
||||
transaction.allocate_payment_entries()
|
||||
transaction.update_allocated_amount()
|
||||
transaction.set_status()
|
||||
transaction.save()
|
||||
|
||||
return transaction
|
||||
|
@ -3,12 +3,11 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
|
||||
class BankTransaction(StatusUpdater):
|
||||
class BankTransaction(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@ -50,6 +49,15 @@ class BankTransaction(StatusUpdater):
|
||||
def validate(self):
|
||||
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):
|
||||
"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
|
||||
if not self.payment_entries:
|
||||
@ -83,12 +91,13 @@ class BankTransaction(StatusUpdater):
|
||||
self.validate_duplicate_references()
|
||||
self.allocate_payment_entries()
|
||||
self.update_allocated_amount()
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
|
||||
self.set_status(update=True)
|
||||
self.set_status()
|
||||
|
||||
def add_payment_entries(self, vouchers):
|
||||
"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
|
||||
):
|
||||
return
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
elif doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
if doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
return
|
||||
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
elif doctype == "Bank Transaction":
|
||||
# For when a second bank transaction has fixed another, e.g. refund
|
||||
|
@ -435,8 +435,8 @@ def update_outstanding_amt(
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
frozen_accounts_modifier = frappe.db.get_single_value(
|
||||
"Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if not frozen_accounts_modifier:
|
||||
|
@ -747,6 +747,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
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'];
|
||||
|
||||
return frappe.call({
|
||||
|
@ -223,6 +223,7 @@
|
||||
"fieldname": "party_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Party Balance",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -759,7 +760,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:07:20.887885",
|
||||
"modified": "2024-01-08 13:17:15.744754",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
@ -50,6 +50,88 @@ class InvalidPaymentEntry(ValidationError):
|
||||
|
||||
|
||||
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):
|
||||
super(PaymentEntry, self).__init__(*args, **kwargs)
|
||||
if not self.is_new():
|
||||
@ -256,6 +338,7 @@ class PaymentEntry(AccountsController):
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
"vouchers": vouchers,
|
||||
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
|
||||
},
|
||||
validate=True,
|
||||
)
|
||||
@ -1628,11 +1711,16 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
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"):
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
get_party_account(args.get("party_type"), args.get("party"), args.get("company")),
|
||||
party_account,
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
|
@ -527,7 +527,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
|
||||
values.extend(warehouses)
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
|
@ -296,6 +296,18 @@ class PurchaseInvoice(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("rejected_warehouse", "items", "rejected_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):
|
||||
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)
|
||||
|
||||
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(
|
||||
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
|
||||
@ -572,7 +584,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def pr_required(self):
|
||||
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(
|
||||
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
|
||||
@ -1104,17 +1116,6 @@ class PurchaseInvoice(BuyingController):
|
||||
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 (
|
||||
self.auto_accounting_for_stock
|
||||
and self.is_opening == "No"
|
||||
@ -1156,17 +1157,24 @@ class PurchaseInvoice(BuyingController):
|
||||
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(
|
||||
"Asset",
|
||||
filters={"purchase_invoice": self.name, "item_code": item.item_code},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
|
||||
frappe.db.set_value(
|
||||
"Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity
|
||||
"Asset",
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
def make_stock_adjustment_entry(
|
||||
|
@ -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.test_purchase_order import create_purchase_order
|
||||
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.exceptions import InvalidCurrency
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
@ -51,6 +51,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
def tearDown(self):
|
||||
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):
|
||||
"""
|
||||
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})
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||
unlink_enabled = frappe.db.get_single_value(
|
||||
"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")
|
||||
frappe.db.set_value(
|
||||
@ -1422,7 +1432,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
pay.cancel()
|
||||
|
||||
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)
|
||||
|
||||
@ -2114,7 +2124,7 @@ def make_purchase_invoice(**args):
|
||||
bundle_id = None
|
||||
if args.get("batch_no") or args.get("serial_no"):
|
||||
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"
|
||||
if args.get("batch_no"):
|
||||
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_name": args.item_name,
|
||||
"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,
|
||||
"rejected_qty": args.rejected_qty or 0,
|
||||
"rate": args.rate or 50,
|
||||
|
@ -898,8 +898,8 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
frm.events.append_time_log(frm, timesheet, 1.0);
|
||||
}
|
||||
});
|
||||
frm.refresh_field("timesheets");
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
frm.refresh();
|
||||
},
|
||||
|
||||
async get_exchange_rate(frm, from_currency, to_currency) {
|
||||
|
@ -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 (
|
||||
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.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
|
||||
@ -72,6 +72,16 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
def tearDownClass(self):
|
||||
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):
|
||||
w = frappe.copy_doc(test_records[0])
|
||||
w.docstatus = 0
|
||||
@ -3636,7 +3646,7 @@ def create_sales_invoice(**args):
|
||||
bundle_id = None
|
||||
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
|
||||
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"
|
||||
if args.get("batch_no"):
|
||||
batches = frappe._dict({args.batch_no: qty})
|
||||
@ -3668,7 +3678,7 @@ def create_sales_invoice(**args):
|
||||
"description": args.description or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"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",
|
||||
"stock_uom": args.uom or "Nos",
|
||||
"rate": args.rate if args.get("rate") is not None else 100,
|
||||
|
@ -654,10 +654,10 @@ def check_freezing_date(posting_date, adv_adj=False):
|
||||
Hence stop admin to bypass if accounts are freezed
|
||||
"""
|
||||
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:
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
frozen_accounts_modifier = frappe.db.get_single_value(
|
||||
"Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
if getdate(posting_date) <= getdate(acc_frozen_upto) and (
|
||||
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
|
||||
|
@ -55,8 +55,8 @@ class ReceivablePayableReport(object):
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
self.set_defaults()
|
||||
self.party_naming_by = frappe.db.get_value(
|
||||
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
||||
self.party_naming_by = frappe.db.get_single_value(
|
||||
args.get("naming_by")[0], args.get("naming_by")[1]
|
||||
)
|
||||
self.get_columns()
|
||||
self.get_data()
|
||||
|
@ -24,8 +24,8 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
def run(self, args):
|
||||
self.account_type = args.get("account_type")
|
||||
self.party_type = get_party_types_from_account_type(self.account_type)
|
||||
self.party_naming_by = frappe.db.get_value(
|
||||
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
||||
self.party_naming_by = frappe.db.get_single_value(
|
||||
args.get("naming_by")[0], args.get("naming_by")[1]
|
||||
)
|
||||
self.get_columns()
|
||||
self.get_data(args)
|
||||
|
@ -128,7 +128,7 @@ frappe.query_reports["Consolidated Financial Statement"] = {
|
||||
}
|
||||
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (!data.parent_account) {
|
||||
if (data && !data.parent_account) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
|
@ -21,8 +21,8 @@ class PartyLedgerSummaryReport(object):
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
self.filters.party_type = args.get("party_type")
|
||||
self.party_naming_by = frappe.db.get_value(
|
||||
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
||||
self.party_naming_by = frappe.db.get_single_value(
|
||||
args.get("naming_by")[0], args.get("naming_by")[1]
|
||||
)
|
||||
|
||||
self.get_gl_entries()
|
||||
|
@ -368,7 +368,7 @@ def filter_invoices_based_on_dimensions(filters, query, parent_doc):
|
||||
dimension.document_type, filters.get(dimension.fieldname)
|
||||
)
|
||||
fieldname = dimension.fieldname
|
||||
query = query.where(parent_doc[fieldname] == filters.fieldname)
|
||||
query = query.where(parent_doc[fieldname].isin(filters[fieldname]))
|
||||
return query
|
||||
|
||||
|
||||
|
@ -1280,7 +1280,7 @@ def parse_naming_series_variable(doc, variable):
|
||||
else:
|
||||
date = getdate()
|
||||
company = None
|
||||
return get_fiscal_year(date=date, company=company)[0]
|
||||
return get_fiscal_year(date=date, company=company).name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -571,10 +571,16 @@ frappe.ui.form.on('Asset', {
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
frm.set_value('gross_purchase_amount', item.base_net_rate + item.item_tax_amount);
|
||||
frm.set_value('purchase_receipt_amount', item.base_net_rate + item.item_tax_amount);
|
||||
item.asset_location && frm.set_value('location', item.asset_location);
|
||||
var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
|
||||
var asset_quantity = is_grouped_asset ? item.qty : 1;
|
||||
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);
|
||||
if(item.asset_location) { frm.set_value('location', item.asset_location); }
|
||||
|
||||
},
|
||||
|
||||
set_depreciation_rate: function(frm, row) {
|
||||
|
@ -202,9 +202,9 @@
|
||||
"fieldname": "purchase_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Purchase Date",
|
||||
"mandatory_depends_on": "eval:!doc.is_existing_asset",
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
|
||||
"reqd": 1
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
||||
},
|
||||
{
|
||||
"fieldname": "disposal_date",
|
||||
@ -227,15 +227,15 @@
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset",
|
||||
"reqd": 1
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset"
|
||||
},
|
||||
{
|
||||
"fieldname": "available_for_use_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Available-for-use Date",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@ -590,7 +590,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2023-12-21 16:46:20.732869",
|
||||
"modified": "2024-01-05 17:36:53.131512",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
@ -57,7 +57,7 @@ class Asset(AccountsController):
|
||||
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
|
||||
asset_owner_company: DF.Link | None
|
||||
asset_quantity: DF.Int
|
||||
available_for_use_date: DF.Date
|
||||
available_for_use_date: DF.Date | None
|
||||
booked_fixed_asset: DF.Check
|
||||
calculate_depreciation: DF.Check
|
||||
capitalized_in: DF.Link | None
|
||||
@ -92,7 +92,7 @@ class Asset(AccountsController):
|
||||
number_of_depreciations_booked: DF.Int
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
policy_number: DF.Data | None
|
||||
purchase_date: DF.Date
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_amount: DF.Currency
|
||||
@ -316,7 +316,12 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
|
||||
|
||||
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(
|
||||
_("Please create purchase receipt or purchase invoice for the item {0}").format(
|
||||
self.item_code
|
||||
@ -329,7 +334,7 @@ class Asset(AccountsController):
|
||||
and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock")
|
||||
):
|
||||
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:
|
||||
|
@ -19,6 +19,7 @@ from frappe.utils import (
|
||||
)
|
||||
from frappe.utils.user import get_users_with_role
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
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):
|
||||
# Return if automatic booking of asset depreciation is disabled
|
||||
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
|
||||
|
||||
@ -522,6 +523,13 @@ def depreciate_asset(asset_doc, date, notes):
|
||||
|
||||
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):
|
||||
if not asset_doc.calculate_depreciation:
|
||||
|
@ -86,12 +86,12 @@ class AssetCategory(Document):
|
||||
if selected_key_type not in expected_key_types:
|
||||
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(
|
||||
d.idx,
|
||||
frappe.unscrub(key_to_match),
|
||||
frappe.bold(selected_account),
|
||||
frappe.bold(expected_key_types),
|
||||
frappe.bold(" or ".join(expected_key_types)),
|
||||
),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
@ -9,6 +9,7 @@
|
||||
"field_order": [
|
||||
"asset",
|
||||
"naming_series",
|
||||
"company",
|
||||
"column_break_2",
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
@ -193,12 +194,20 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on shifts",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "asset.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 00:57:00.461998",
|
||||
"modified": "2024-01-08 16:31:04.533928",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Depreciation Schedule",
|
||||
|
@ -35,6 +35,7 @@ class AssetDepreciationSchedule(Document):
|
||||
|
||||
amended_from: DF.Link | None
|
||||
asset: DF.Link
|
||||
company: DF.Link | None
|
||||
daily_prorata_based: DF.Check
|
||||
depreciation_method: DF.Literal[
|
||||
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"
|
||||
@ -340,10 +341,7 @@ class AssetDepreciationSchedule(Document):
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
and get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset_doc, value_after_depreciation, row, False
|
||||
)
|
||||
== row.rate_of_depreciation
|
||||
and not self.flags.wdv_it_act_applied
|
||||
):
|
||||
from_date = add_days(
|
||||
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
|
||||
)
|
||||
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(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
rate_of_depreciation,
|
||||
fb_row.frequency_of_depreciation,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
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(
|
||||
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))
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
rate_of_depreciation,
|
||||
frequency_of_depreciation,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
):
|
||||
if cint(frequency_of_depreciation) == 12:
|
||||
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
|
||||
return 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,
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||
if schedule_idx == 0:
|
||||
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
|
||||
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
|
||||
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
|
||||
elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1:
|
||||
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:
|
||||
return prev_depreciation_amount
|
||||
else:
|
||||
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
|
||||
if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0:
|
||||
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:
|
||||
return prev_depreciation_amount
|
||||
|
@ -94,7 +94,6 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
|
||||
"fieldname": "daily_prorata_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on daily pro-rata"
|
||||
@ -110,7 +109,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 00:57:07.579777",
|
||||
"modified": "2023-12-29 08:49:39.876439",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
@ -214,7 +214,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-28 13:01:18.403492",
|
||||
"modified": "2024-01-05 15:26:02.320942",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
@ -238,6 +238,41 @@
|
||||
"role": "Purchase Manager",
|
||||
"share": 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",
|
||||
|
@ -29,6 +29,8 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_purchase_order_qty(self):
|
||||
po = create_purchase_order(qty=1, do_not_save=True)
|
||||
|
||||
# NonNegativeError with qty=-1
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
@ -39,9 +41,15 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
)
|
||||
self.assertRaises(frappe.NonNegativeError, po.save)
|
||||
|
||||
# InvalidQtyError with qty=0
|
||||
po.items[1].qty = 0
|
||||
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):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
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",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"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,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get("include_exploded_items", 1),
|
||||
|
@ -65,6 +65,7 @@ class RequestforQuotation(BuyingController):
|
||||
def validate(self):
|
||||
self.validate_duplicate_supplier()
|
||||
self.validate_supplier_list()
|
||||
super(RequestforQuotation, self).validate_qty_is_not_zero()
|
||||
validate_for_items(self)
|
||||
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
|
||||
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(
|
||||
"Supplier", for_supplier, source.company
|
||||
)
|
||||
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value(
|
||||
"Buying Settings", None, "buying_price_list"
|
||||
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_single_value(
|
||||
"Buying Settings", "buying_price_list"
|
||||
)
|
||||
set_missing_values(source, target_doc)
|
||||
|
||||
@ -398,7 +399,7 @@ def create_supplier_quotation(doc):
|
||||
"currency": doc.get("currency")
|
||||
or get_party_account_currency("Supplier", doc.get("supplier"), doc.get("company")),
|
||||
"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"))
|
||||
|
@ -14,6 +14,7 @@ from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
|
||||
get_pdf,
|
||||
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.test_opportunity import make_opportunity
|
||||
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):
|
||||
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):
|
||||
rfq = make_request_for_quotation()
|
||||
|
||||
@ -161,14 +172,17 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
|
||||
"description": "_Test Item",
|
||||
"uom": args.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,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
},
|
||||
)
|
||||
|
||||
rfq.submit()
|
||||
if not args.do_not_save:
|
||||
rfq.insert()
|
||||
if not args.do_not_submit:
|
||||
rfq.submit()
|
||||
|
||||
return rfq
|
||||
|
||||
|
@ -5,8 +5,21 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
|
||||
|
||||
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):
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
|
||||
|
@ -44,11 +44,6 @@ def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
def validate_for_items(doc) -> None:
|
||||
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
|
||||
item = validate_item_and_get_basic_data(row=d)
|
||||
validate_stock_item_warehouse(row=d, item=item)
|
||||
|
@ -975,13 +975,15 @@ class AccountsController(TransactionBase):
|
||||
return flt(args.get(field, 0) / (args.get("conversion_rate") or self.get("conversion_rate", 1)))
|
||||
|
||||
def validate_qty_is_not_zero(self):
|
||||
if self.doctype == "Purchase Receipt":
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
if self.doctype == "Purchase Receipt" and item.rejected_qty:
|
||||
continue
|
||||
|
||||
if not flt(item.qty):
|
||||
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"),
|
||||
exc=InvalidQtyError,
|
||||
)
|
||||
@ -1960,7 +1962,7 @@ class AccountsController(TransactionBase):
|
||||
self.remove(item)
|
||||
|
||||
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 = ""
|
||||
return
|
||||
|
||||
@ -2143,7 +2145,7 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
if not flt(new_data.get("qty")):
|
||||
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"))
|
||||
),
|
||||
title=_("Invalid Qty"),
|
||||
|
@ -744,11 +744,8 @@ class BuyingController(SubcontractingController):
|
||||
item_data = frappe.db.get_value(
|
||||
"Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1
|
||||
)
|
||||
|
||||
if is_grouped_asset:
|
||||
purchase_amount = flt(row.base_amount + row.item_tax_amount)
|
||||
else:
|
||||
purchase_amount = flt(row.base_rate + row.item_tax_amount)
|
||||
asset_quantity = row.qty if is_grouped_asset else 1
|
||||
purchase_amount = flt(row.valuation_rate) * asset_quantity
|
||||
|
||||
asset = frappe.get_doc(
|
||||
{
|
||||
@ -764,7 +761,7 @@ class BuyingController(SubcontractingController):
|
||||
"calculate_depreciation": 0,
|
||||
"purchase_receipt_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_invoice": self.name if self.doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ from frappe.utils import flt, format_datetime, get_datetime
|
||||
import erpnext
|
||||
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.utils import get_incoming_rate
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class StockOverReturnError(frappe.ValidationError):
|
||||
@ -116,7 +116,12 @@ def validate_returned_items(doc):
|
||||
ref = valid_items.get(d.item_code, frappe._dict())
|
||||
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(
|
||||
_("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format(
|
||||
d.idx, doc.doctype, doc.return_against
|
||||
|
@ -295,9 +295,6 @@ class SellingController(StockController):
|
||||
def get_item_list(self):
|
||||
il = []
|
||||
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):
|
||||
for p in self.get("packed_items"):
|
||||
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 [])
|
||||
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 (
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
):
|
||||
|
@ -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'",
|
||||
],
|
||||
],
|
||||
"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": [
|
||||
["Draft", None],
|
||||
["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],
|
||||
|
@ -76,7 +76,7 @@ class Appointment(Document):
|
||||
self.create_calendar_event()
|
||||
else:
|
||||
# Set status to unverified
|
||||
self.status = "Unverified"
|
||||
self.db_set("status", "Unverified")
|
||||
# Send email to confirm
|
||||
self.send_confirmation_email()
|
||||
|
||||
|
@ -129,9 +129,7 @@ class TallyMigration(Document):
|
||||
self.default_cost_center, self.default_round_off_account = frappe.db.get_value(
|
||||
"Company", self.erpnext_company, ["cost_center", "round_off_account"]
|
||||
)
|
||||
self.default_warehouse = frappe.db.get_value(
|
||||
"Stock Settings", "Stock Settings", "default_warehouse"
|
||||
)
|
||||
self.default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse")
|
||||
|
||||
def _process_master_data(self):
|
||||
def get_company_name(collection):
|
||||
|
@ -489,6 +489,7 @@ bank_reconciliation_doctypes = [
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
]
|
||||
|
||||
accounting_dimension_doctypes = [
|
||||
|
82664
erpnext/locale/af.po
Normal file
82664
erpnext/locale/af.po
Normal file
File diff suppressed because it is too large
Load Diff
82539
erpnext/locale/ar.po
Normal file
82539
erpnext/locale/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
82887
erpnext/locale/de.po
Normal file
82887
erpnext/locale/de.po
Normal file
File diff suppressed because it is too large
Load Diff
82816
erpnext/locale/es.po
Normal file
82816
erpnext/locale/es.po
Normal file
File diff suppressed because it is too large
Load Diff
82461
erpnext/locale/fi.po
Normal file
82461
erpnext/locale/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
82915
erpnext/locale/fr.po
Normal file
82915
erpnext/locale/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
82588
erpnext/locale/id.po
Normal file
82588
erpnext/locale/id.po
Normal file
File diff suppressed because it is too large
Load Diff
82855
erpnext/locale/it.po
Normal file
82855
erpnext/locale/it.po
Normal file
File diff suppressed because it is too large
Load Diff
82018
erpnext/locale/main.pot
Normal file
82018
erpnext/locale/main.pot
Normal file
File diff suppressed because it is too large
Load Diff
82673
erpnext/locale/nl.po
Normal file
82673
erpnext/locale/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
82620
erpnext/locale/pl.po
Normal file
82620
erpnext/locale/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
82701
erpnext/locale/pt.po
Normal file
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
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
82620
erpnext/locale/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
82456
erpnext/locale/tr.po
Normal file
82456
erpnext/locale/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
82508
erpnext/locale/vi.po
Normal file
82508
erpnext/locale/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
81950
erpnext/locale/zh.po
Normal file
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
81952
erpnext/locale/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -117,7 +117,7 @@ class MaintenanceSchedule(TransactionBase):
|
||||
self.update_amc_date(serial_nos, d.end_date)
|
||||
|
||||
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)
|
||||
try:
|
||||
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))
|
||||
)
|
||||
|
||||
scheduled_date = frappe.db.sql(
|
||||
"""select scheduled_date from
|
||||
`tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and
|
||||
parent=%s""",
|
||||
(d.sales_person, d.item_code, self.name),
|
||||
as_dict=1,
|
||||
scheduled_date = frappe.db.get_all(
|
||||
"Maintenance Schedule Detail",
|
||||
{"parent": self.name, "item_code": d.item_code},
|
||||
["scheduled_date"],
|
||||
as_list=False,
|
||||
)
|
||||
|
||||
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))
|
||||
elif not d.no_of_visits:
|
||||
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):
|
||||
throw(_("Start date should be less than end date for Item {0}").format(d.item_code))
|
||||
@ -452,20 +449,28 @@ 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):
|
||||
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):
|
||||
target.maintenance_type = "Scheduled"
|
||||
target.maintenance_schedule_detail = s_id
|
||||
|
||||
def update_serial(source, target, parent):
|
||||
if source.serial_and_batch_bundle:
|
||||
serial_nos = frappe.get_doc(
|
||||
"Serial and Batch Bundle", source.serial_and_batch_bundle
|
||||
).get_serial_nos()
|
||||
if source.item_reference:
|
||||
if sbb := frappe.db.get_value(
|
||||
"Maintenance Schedule Item", source.item_reference, "serial_and_batch_bundle"
|
||||
):
|
||||
serial_nos = frappe.get_doc("Serial and Batch Bundle", sbb).get_serial_nos()
|
||||
|
||||
if len(serial_nos) == 1:
|
||||
target.serial_no = serial_nos[0]
|
||||
else:
|
||||
target.serial_no = ""
|
||||
if len(serial_nos) == 1:
|
||||
target.serial_no = serial_nos[0]
|
||||
else:
|
||||
target.serial_no = ""
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Maintenance Schedule",
|
||||
@ -477,10 +482,13 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
"postprocess": update_status_and_detail,
|
||||
},
|
||||
"Maintenance Schedule Item": {
|
||||
"Maintenance Schedule Detail": {
|
||||
"doctype": "Maintenance Visit Purpose",
|
||||
"condition": lambda doc: doc.item_name == item_name if item_name else True,
|
||||
"field_map": {"sales_person": "service_person"},
|
||||
"condition": condition,
|
||||
"field_map": {
|
||||
"sales_person": "service_person",
|
||||
"name": "maintenance_schedule_detail",
|
||||
},
|
||||
"postprocess": update_serial,
|
||||
},
|
||||
},
|
||||
|
@ -56,20 +56,39 @@ class MaintenanceVisit(TransactionBase):
|
||||
frappe.throw(_("Add Items in the Purpose Table"), title=_("Purposes Required"))
|
||||
|
||||
def validate_maintenance_date(self):
|
||||
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
|
||||
item_ref = frappe.db.get_value(
|
||||
"Maintenance Schedule Detail", self.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 self.maintenance_type == "Scheduled":
|
||||
if self.maintenance_schedule_detail:
|
||||
item_ref = frappe.db.get_value(
|
||||
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference"
|
||||
)
|
||||
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))
|
||||
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))
|
||||
)
|
||||
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):
|
||||
self.validate_serial_no()
|
||||
@ -82,6 +101,7 @@ class MaintenanceVisit(TransactionBase):
|
||||
if not cancel:
|
||||
status = self.completion_status
|
||||
actual_date = self.mntc_date
|
||||
|
||||
if self.maintenance_schedule_detail:
|
||||
frappe.db.set_value(
|
||||
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status
|
||||
@ -89,6 +109,21 @@ class MaintenanceVisit(TransactionBase):
|
||||
frappe.db.set_value(
|
||||
"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):
|
||||
if not self.maintenance_schedule:
|
||||
|
@ -17,7 +17,8 @@
|
||||
"work_details",
|
||||
"work_done",
|
||||
"prevdoc_doctype",
|
||||
"prevdoc_docname"
|
||||
"prevdoc_docname",
|
||||
"maintenance_schedule_detail"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -49,6 +50,8 @@
|
||||
"options": "Serial No"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.description",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
@ -56,7 +59,6 @@
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_width": "300px",
|
||||
"reqd": 1,
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
@ -103,12 +105,19 @@
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "maintenance_schedule_detail",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Maintenance Schedule Detail",
|
||||
"options": "Maintenance Schedule Detail"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-27 11:09:33.114458",
|
||||
"modified": "2024-01-05 21:46:53.239830",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Maintenance",
|
||||
"name": "Maintenance Visit Purpose",
|
||||
|
@ -14,9 +14,10 @@ class MaintenanceVisitPurpose(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
description: DF.TextEditor
|
||||
description: DF.TextEditor | None
|
||||
item_code: DF.Link | None
|
||||
item_name: DF.Data | None
|
||||
maintenance_schedule_detail: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
@ -12,12 +12,13 @@
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"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",
|
||||
"module": "Manufacturing",
|
||||
"name": "Completed Operation",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"parent_document_type": "Work Order",
|
||||
"time_interval": "Quarterly",
|
||||
"timeseries": 1,
|
||||
"timespan": "Last Year",
|
||||
|
@ -27,6 +27,28 @@ test_dependencies = ["Item", "Quality Inspection Template"]
|
||||
|
||||
|
||||
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
|
||||
def test_get_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||
|
@ -86,10 +86,12 @@ def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
|
||||
if new_bom == d.parent:
|
||||
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
|
||||
|
||||
bom_list.append(d.parent)
|
||||
if d.parent not in tuple(bom_list):
|
||||
bom_list.append(d.parent)
|
||||
|
||||
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:
|
||||
|
@ -57,6 +57,68 @@ class TestBOMUpdateLog(FrappeTestCase):
|
||||
log.reload()
|
||||
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():
|
||||
"""
|
||||
|
@ -173,7 +173,7 @@ frappe.ui.form.on('Production Plan', {
|
||||
method: "set_status",
|
||||
freeze: true,
|
||||
doc: frm.doc,
|
||||
args: {close : close},
|
||||
args: {close : close, update_bin: true},
|
||||
callback: function() {
|
||||
frm.reload_doc();
|
||||
}
|
||||
|
@ -579,7 +579,7 @@ class ProductionPlan(Document):
|
||||
frappe.delete_doc("Work Order", d.name)
|
||||
|
||||
@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)
|
||||
|
||||
if close:
|
||||
@ -599,7 +599,7 @@ class ProductionPlan(Document):
|
||||
if close is not None:
|
||||
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()
|
||||
|
||||
def update_ordered_status(self):
|
||||
@ -646,6 +646,10 @@ class ProductionPlan(Document):
|
||||
"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:
|
||||
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
|
||||
else:
|
||||
item_details.update(
|
||||
{
|
||||
"qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse), {}).get("qty"))
|
||||
+ (flt(d.planned_qty) - flt(d.ordered_qty))
|
||||
}
|
||||
{"qty": flt(item_dict.get(key, {}).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
|
||||
|
||||
|
@ -672,7 +672,7 @@ class TestProductionPlan(FrappeTestCase):
|
||||
items_data = pln.get_production_items()
|
||||
|
||||
# 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
|
||||
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"))
|
||||
|
||||
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)
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
self.assertAlmostEqual(after_qty, before_qty - 10)
|
||||
|
||||
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)
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
|
@ -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"):
|
||||
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)
|
||||
|
||||
|
||||
|
@ -45,7 +45,7 @@ def execute():
|
||||
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:
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
|
||||
|
@ -2,12 +2,7 @@ import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
settings = frappe.db.get_value(
|
||||
"Selling Settings",
|
||||
"Selling Settings",
|
||||
["campaign_naming_by", "close_opportunity_after_days", "default_valid_till"],
|
||||
as_dict=True,
|
||||
)
|
||||
settings = frappe.db.get_singles_dict("Selling Settings", cast=True)
|
||||
|
||||
frappe.reload_doc("crm", "doctype", "crm_settings")
|
||||
if settings:
|
||||
|
@ -131,6 +131,7 @@
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "expected_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Start Date",
|
||||
@ -453,7 +454,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2023-08-28 22:27:28.370849",
|
||||
"modified": "2024-01-08 16:01:34.598258",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
|
@ -19,6 +19,62 @@ from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
|
||||
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):
|
||||
self.set_onload(
|
||||
"activity_summary",
|
||||
@ -314,18 +370,16 @@ def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
|
||||
def get_project_list(
|
||||
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)
|
||||
|
||||
ignore_permissions = False
|
||||
if is_website_user():
|
||||
if is_website_user() and frappe.session.user != "Guest":
|
||||
if not filters:
|
||||
filters = []
|
||||
|
||||
if customers:
|
||||
filters.append([doctype, "customer", "in", customers])
|
||||
|
||||
ignore_permissions = True
|
||||
ignore_permissions = True
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
|
@ -153,6 +153,7 @@
|
||||
"label": "Timeline"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "exp_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Start Date",
|
||||
@ -398,7 +399,7 @@
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2023-11-20 11:42:41.884069",
|
||||
"modified": "2024-01-08 16:00:41.296203",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Task",
|
||||
|
@ -790,7 +790,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
if (me.frm.doc.price_list_currency == company_currency) {
|
||||
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) {
|
||||
me.frm.set_value("letter_head", company_doc.default_letter_head);
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ erpnext.accounts.dimensions = {
|
||||
},
|
||||
callback: function(r) {
|
||||
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.setup_filters(frm, doctype);
|
||||
}
|
||||
|
@ -370,15 +370,16 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_
|
||||
)
|
||||
|
||||
# sales team
|
||||
for d in customer.get("sales_team") or []:
|
||||
target.append(
|
||||
"sales_team",
|
||||
{
|
||||
"sales_person": d.sales_person,
|
||||
"allocated_percentage": d.allocated_percentage or None,
|
||||
"commission_rate": d.commission_rate,
|
||||
},
|
||||
)
|
||||
if not target.get("sales_team"):
|
||||
for d in customer.get("sales_team") or []:
|
||||
target.append(
|
||||
"sales_team",
|
||||
{
|
||||
"sales_person": d.sales_person,
|
||||
"allocated_percentage": d.allocated_percentage or None,
|
||||
"commission_rate": d.commission_rate,
|
||||
},
|
||||
)
|
||||
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
target.delivery_date = nowdate()
|
||||
|
@ -5,10 +5,22 @@ import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
|
||||
test_dependencies = ["Product Bundle"]
|
||||
|
||||
|
||||
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):
|
||||
quotation = make_quotation(do_not_save=1)
|
||||
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",
|
||||
"warehouse": args.warehouse,
|
||||
"qty": args.qty or 10,
|
||||
"qty": args.qty if args.qty is not None else 10,
|
||||
"uom": args.uom or None,
|
||||
"rate": args.rate or 100,
|
||||
},
|
||||
|
@ -582,17 +582,17 @@ class SalesOrder(SellingController):
|
||||
|
||||
def set_indicator(self):
|
||||
"""Set indicator for portal"""
|
||||
if self.per_billed < 100 and self.per_delivered < 100:
|
||||
self.indicator_color = "orange"
|
||||
self.indicator_title = _("Not Paid and Not Delivered")
|
||||
self.indicator_color = {
|
||||
"Draft": "red",
|
||||
"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_color = "orange"
|
||||
self.indicator_title = _("Paid and Not Delivered")
|
||||
|
||||
else:
|
||||
self.indicator_color = "green"
|
||||
self.indicator_title = _("Paid")
|
||||
self.indicator_title = _(self.status)
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
|
||||
|
@ -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.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 (
|
||||
make_maintenance_schedule,
|
||||
)
|
||||
@ -35,8 +35,8 @@ class TestSalesOrder(FrappeTestCase):
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.unlink_setting = int(
|
||||
frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order"
|
||||
frappe.db.get_single_value(
|
||||
"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)
|
||||
|
||||
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):
|
||||
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",
|
||||
"warehouse": args.warehouse,
|
||||
"qty": args.qty or 10,
|
||||
"qty": args.qty if args.qty is not None else 10,
|
||||
"uom": args.uom or None,
|
||||
"price_list_rate": args.price_list_rate or None,
|
||||
"discount_percentage": args.discount_percentage or None,
|
||||
|
@ -13,7 +13,7 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
# 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)
|
||||
|
||||
data = []
|
||||
|
@ -87,7 +87,7 @@ class Employee(NestedSet):
|
||||
def update_user_permissions(self):
|
||||
if not self.create_user_permission:
|
||||
return
|
||||
if not has_permission("User Permission", ptype="write", raise_exception=False):
|
||||
if not has_permission("User Permission", ptype="write", print_logs=False):
|
||||
return
|
||||
|
||||
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):
|
||||
# called via User hook
|
||||
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
|
||||
employee = frappe.get_doc("Employee", {"user_id": doc.name})
|
||||
employee.update_user_permissions()
|
||||
|
@ -9,8 +9,6 @@ from frappe.utils import cint
|
||||
def boot_session(bootinfo):
|
||||
"""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":
|
||||
update_page_info(bootinfo)
|
||||
|
||||
|
@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
|
||||
& (
|
||||
(table.from_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)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -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.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.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
@ -42,6 +43,16 @@ from erpnext.stock.stock_ledger import get_previous_sle
|
||||
|
||||
|
||||
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):
|
||||
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
|
||||
)
|
||||
|
||||
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):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
@ -1539,7 +1569,7 @@ def create_delivery_note(**args):
|
||||
if dn.is_return:
|
||||
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
|
||||
batches = {}
|
||||
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",
|
||||
"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,
|
||||
"conversion_factor": 1.0,
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
|
@ -202,6 +202,7 @@
|
||||
"label": "Allow Alternative Item"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"bold": 1,
|
||||
"default": "1",
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
@ -239,6 +240,7 @@
|
||||
"label": "Standard Selling Rate"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"default": "0",
|
||||
"fieldname": "is_fixed_asset",
|
||||
"fieldtype": "Check",
|
||||
@ -246,6 +248,7 @@
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"depends_on": "is_fixed_asset",
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
@ -888,7 +891,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2023-09-18 15:41:32.688051",
|
||||
"modified": "2024-01-08 18:09:30.225085",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
@ -961,4 +964,4 @@
|
||||
"states": [],
|
||||
"title_field": "item_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
@ -635,8 +635,8 @@ class Item(Document):
|
||||
def recalculate_bin_qty(self, new_name):
|
||||
from erpnext.stock.stock_balance import repost_stock
|
||||
|
||||
existing_allow_negative_stock = frappe.db.get_value(
|
||||
"Stock Settings", None, "allow_negative_stock"
|
||||
existing_allow_negative_stock = frappe.db.get_single_value(
|
||||
"Stock Settings", "allow_negative_stock"
|
||||
)
|
||||
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
|
||||
|
||||
|
@ -194,7 +194,8 @@ class LandedCostVoucher(Document):
|
||||
for d in self.get("purchase_receipts"):
|
||||
doc = frappe.get_doc(d.receipt_document_type, d.receipt_document)
|
||||
# check if there are {qty} assets created and linked to this receipt document
|
||||
self.validate_asset_qty_and_status(d.receipt_document_type, doc)
|
||||
if self.docstatus != 2:
|
||||
self.validate_asset_qty_and_status(d.receipt_document_type, doc)
|
||||
|
||||
# set landed cost voucher amount in pr item
|
||||
doc.set_landed_cost_voucher_amount()
|
||||
@ -239,20 +240,20 @@ class LandedCostVoucher(Document):
|
||||
},
|
||||
fields=["name", "docstatus"],
|
||||
)
|
||||
if not docs or len(docs) != item.qty:
|
||||
if not docs or len(docs) < item.qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document."
|
||||
).format(item.receipt_document, item.qty)
|
||||
"There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document."
|
||||
).format(len(docs), item.receipt_document, item.qty)
|
||||
)
|
||||
if docs:
|
||||
for d in docs:
|
||||
if d.docstatus == 1:
|
||||
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(
|
||||
item.receipt_document, item.item_code, item.receipt_document_type
|
||||
item.receipt_document_type, item.receipt_document, item.item_code
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -9,6 +9,7 @@ import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
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.material_request.material_request import (
|
||||
make_in_transit_stock_entry,
|
||||
@ -20,6 +21,17 @@ from erpnext.stock.doctype.material_request.material_request import (
|
||||
|
||||
|
||||
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):
|
||||
mr = frappe.copy_doc(test_records[0]).insert()
|
||||
|
||||
|
@ -39,11 +39,11 @@ class PriceList(Document):
|
||||
|
||||
def set_default_if_missing(self):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def update_item_price(self):
|
||||
|
@ -13,6 +13,7 @@ from pypika import functions as fn
|
||||
import erpnext
|
||||
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_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
|
||||
@ -302,7 +303,7 @@ class PurchaseReceipt(BuyingController):
|
||||
)
|
||||
|
||||
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"):
|
||||
if not d.purchase_order:
|
||||
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
|
||||
@ -674,15 +675,16 @@ class PurchaseReceipt(BuyingController):
|
||||
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
||||
|
||||
if d.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(d.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
|
||||
stock_asset_account_name = get_asset_account(
|
||||
account_type, asset_category=d.asset_category, company=self.company
|
||||
)
|
||||
if is_cwip_accounting_enabled(d.asset_category):
|
||||
stock_asset_account_name = get_asset_account(
|
||||
"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 = (
|
||||
flt(d.base_net_amount)
|
||||
@ -719,7 +721,7 @@ class PurchaseReceipt(BuyingController):
|
||||
):
|
||||
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)
|
||||
|
||||
if warehouse_with_no_account:
|
||||
@ -852,11 +854,14 @@ class PurchaseReceipt(BuyingController):
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
purchase_amount = flt(valuation_rate) * asset.asset_quantity
|
||||
frappe.db.set_value(
|
||||
"Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity
|
||||
"Asset",
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
def update_status(self, status):
|
||||
@ -1228,6 +1233,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
"field_map": {
|
||||
"name": "pr_detail",
|
||||
"parent": "purchase_receipt",
|
||||
"qty": "received_qty",
|
||||
"purchase_order_item": "po_detail",
|
||||
"purchase_order": "purchase_order",
|
||||
"is_fixed_asset": "is_fixed_asset",
|
||||
|
@ -9,6 +9,7 @@ from pypika import functions as fn
|
||||
|
||||
import erpnext
|
||||
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.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||
@ -30,6 +31,23 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
def setUp(self):
|
||||
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):
|
||||
"""
|
||||
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.return_against = args.return_against
|
||||
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
|
||||
received_qty = args.received_qty or flt(rejected_qty) + flt(qty)
|
||||
|
||||
|
@ -111,6 +111,9 @@ class QualityInspection(Document):
|
||||
def on_cancel(self):
|
||||
self.update_qc_reference()
|
||||
|
||||
def on_trash(self):
|
||||
self.update_qc_reference()
|
||||
|
||||
def validate_readings_status_mandatory(self):
|
||||
for reading in self.readings:
|
||||
if not reading.status:
|
||||
|
@ -250,6 +250,33 @@ class TestQualityInspection(FrappeTestCase):
|
||||
qa.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):
|
||||
args = frappe._dict(args)
|
||||
|
@ -140,12 +140,7 @@ class RepostItemValuation(Document):
|
||||
return query[0][0] if query else None
|
||||
|
||||
def validate_accounts_freeze(self):
|
||||
acc_settings = frappe.db.get_value(
|
||||
"Accounts Settings",
|
||||
"Accounts Settings",
|
||||
["acc_frozen_upto", "frozen_accounts_modifier"],
|
||||
as_dict=1,
|
||||
)
|
||||
acc_settings = frappe.get_cached_doc("Accounts Settings")
|
||||
if not acc_settings.acc_frozen_upto:
|
||||
return
|
||||
if getdate(self.posting_date) <= getdate(acc_settings.acc_frozen_upto):
|
||||
@ -224,7 +219,7 @@ class RepostItemValuation(Document):
|
||||
|
||||
transaction_status = frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus")
|
||||
if transaction_status == 2:
|
||||
msg = _("Cannot cancel as processing of cancelled documents is pending.")
|
||||
msg = _("Cannot cancel as processing of cancelled documents is pending.")
|
||||
msg += "<br>" + _("Please try again in an hour.")
|
||||
frappe.throw(msg, title=_("Pending processing"))
|
||||
|
||||
|
@ -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)
|
||||
|
||||
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 = []
|
||||
user = frappe.session.user
|
||||
for serial_no in serial_nos:
|
||||
if frappe.db.exists("Serial No", serial_no):
|
||||
continue
|
||||
|
||||
serial_nos_details.append(
|
||||
(
|
||||
serial_no,
|
||||
@ -1046,9 +1050,16 @@ def make_serial_nos(item_code, serial_nos):
|
||||
|
||||
def make_batch_nos(item_code, batch_nos):
|
||||
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")]
|
||||
|
||||
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 = []
|
||||
user = frappe.session.user
|
||||
for batch_no in batch_nos:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user