Merge branch 'develop' into fix-holiday-list

This commit is contained in:
barredterra 2024-01-09 17:46:28 +01:00
commit 58343e5b7c
174 changed files with 1291 additions and 494 deletions

View File

@ -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)

View File

@ -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) {

View File

@ -74,7 +74,7 @@ def create_charts(
# after all accounts are already inserted.
frappe.local.flags.ignore_update_nsm = True
_import_accounts(chart, None, None, root_account=True)
rebuild_tree("Account", "parent_account")
rebuild_tree("Account")
frappe.local.flags.ignore_update_nsm = False
@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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({

View File

@ -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",

View File

@ -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"),

View File

@ -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))
)

View File

@ -552,7 +552,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 +572,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"

View File

@ -14,7 +14,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.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,8 +1237,8 @@ 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_cancel_of_invoice"
)
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
@ -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,

View File

@ -23,7 +23,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_d
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
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,

View File

@ -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"

View File

@ -114,14 +114,12 @@ def _get_party_details(
set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)
)
party = party_details[party_type.lower()]
if not ignore_permissions and not (
frappe.has_permission(party_type, "read", party)
or frappe.has_permission(party_type, "select", party)
):
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party)
if not ignore_permissions:
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details(
@ -637,9 +635,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
return due_date
def validate_due_date(
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
):
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else:

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -251,6 +251,7 @@ def get_journal_entries(filters, args):
)
.where(
(je.voucher_type == "Journal Entry")
& (je.docstatus == 1)
& (journal_account.party == filters.get(args.party))
& (journal_account.account.isin(args.party_account))
)
@ -281,7 +282,9 @@ def get_payment_entries(filters, args):
pe.cost_center,
)
.where(
(pe.party == filters.get(args.party)) & (pe[args.account_fieldname].isin(args.party_account))
(pe.docstatus == 1)
& (pe.party == filters.get(args.party))
& (pe[args.account_fieldname].isin(args.party_account))
)
.orderby(pe.posting_date, pe.name, order=Order.desc)
)

View File

@ -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()

View File

@ -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",

View File

@ -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:

View File

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

View File

@ -3,6 +3,7 @@
import frappe
from frappe.model.document import Document
from frappe.utils import now_datetime
class AssetActivity(Document):
@ -30,5 +31,6 @@ def add_asset_activity(asset, subject):
"asset": asset,
"subject": subject,
"user": frappe.session.user,
"date": now_datetime(),
}
).insert(ignore_permissions=True, ignore_links=True)

View File

@ -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"),
)

View File

@ -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",

View File

@ -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"

View File

@ -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",

View File

@ -452,6 +452,7 @@ class PurchaseOrder(BuyingController):
self.update_requested_qty()
self.update_ordered_qty()
self.update_reserved_qty_for_subcontract()
self.update_subcontracting_order_status()
self.notify_update()
clear_doctype_notifications(self)
@ -627,6 +628,17 @@ class PurchaseOrder(BuyingController):
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
make_subcontracting_order(self.name, save=True, notify=True)
def update_subcontracting_order_status(self):
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
update_subcontracting_order_status as update_sco_status,
)
if self.is_subcontracted and not self.is_old_subcontracting_flow:
sco = frappe.db.get_value("Subcontracting Order", {"purchase_order": self.name, "docstatus": 1})
if sco:
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item"""

View File

@ -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),

View File

@ -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"))

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -129,6 +129,17 @@ class AccountsController(TransactionBase):
if self.doctype in relevant_docs:
self.set_payment_schedule()
def remove_bundle_for_non_stock_invoices(self):
has_sabb = False
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.update_stock:
for item in self.get("items"):
if item.serial_and_batch_bundle:
item.serial_and_batch_bundle = None
has_sabb = True
if has_sabb:
self.remove_serial_and_batch_bundle()
def ensure_supplier_is_not_blocked(self):
is_supplier_payment = self.doctype == "Payment Entry" and self.party_type == "Supplier"
is_buying_invoice = self.doctype in ["Purchase Invoice", "Purchase Order"]
@ -156,6 +167,9 @@ class AccountsController(TransactionBase):
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
if self.get("_action") == "submit":
self.remove_bundle_for_non_stock_invoices()
self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year()
@ -561,18 +575,12 @@ class AccountsController(TransactionBase):
validate_due_date(
self.posting_date,
self.due_date,
"Customer",
self.customer,
self.company,
self.payment_terms_template,
)
elif self.doctype == "Purchase Invoice":
validate_due_date(
self.bill_date or self.posting_date,
self.due_date,
"Supplier",
self.supplier,
self.company,
self.bill_date,
self.payment_terms_template,
)
@ -967,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,
)
@ -3089,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"),

View File

@ -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

View File

@ -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")
):

View File

@ -131,11 +131,6 @@ status_map = {
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'",
],
],
"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"],

View File

@ -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()

View File

@ -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):

View File

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

View File

@ -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,
},
},

View File

@ -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:

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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():
"""

View File

@ -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

View File

@ -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():
@ -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):
"""

View File

@ -156,7 +156,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da
if not frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"):
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)

View File

@ -35,7 +35,7 @@ def execute():
# append list of new department for each company
comp_dict[company.name][department.name] = copy_doc.name
rebuild_tree("Department", "parent_department")
rebuild_tree("Department")
doctypes = ["Asset", "Employee", "Payroll Entry", "Staffing Plan", "Job Opening"]
for d in doctypes:

View File

@ -27,7 +27,7 @@ def execute():
except frappe.DuplicateEntryError:
continue
rebuild_tree("Location", "parent_location")
rebuild_tree("Location")
def get_parent_warehouse_name(warehouse):

View File

@ -4,4 +4,4 @@ from frappe.utils.nestedset import rebuild_tree
def execute():
frappe.reload_doc("setup", "doctype", "company")
rebuild_tree("Company", "parent_company")
rebuild_tree("Company")

View File

@ -41,4 +41,4 @@ def build_tree():
}
).insert(ignore_permissions=True)
rebuild_tree("Supplier Group", "parent_supplier_group")
rebuild_tree("Supplier Group")

View File

@ -18,4 +18,4 @@ def execute():
)
)
rebuild_tree("Department", "parent_department")
rebuild_tree("Department")

View File

@ -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)

View File

@ -2,8 +2,7 @@ import frappe
def execute():
settings = frappe.db.get_value(
"Selling Settings",
settings = frappe.db.get_single_value(
"Selling Settings",
["campaign_naming_by", "close_opportunity_after_days", "default_valid_till"],
as_dict=True,

View File

@ -4,5 +4,5 @@ import frappe
def execute():
subscription = frappe.qb.DocType("Subscription")
frappe.qb.update(subscription).set(
subscription.generate_invoice_at, "Beginning of the currency subscription period"
subscription.generate_invoice_at, "Beginning of the current subscription period"
).where(subscription.generate_invoice_at_period_start == 1).run()

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -454,7 +454,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.weight_uom = '';
item.conversion_factor = 0;
if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
update_stock = cint(me.frm.doc.update_stock);
show_batch_dialog = update_stock;
@ -545,7 +545,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
() => me.toggle_conversion_factor(item),
() => {
if (show_batch_dialog)
if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message &&
@ -1239,6 +1239,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
sync_bundle_data() {
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.sync_bundle_data();
barcode_scanner.remove_item_from_localstorage();
}
}
before_save(doc) {
this.sync_bundle_data();
}
service_start_date(frm, cdt, cdn) {
var child = locals[cdt][cdn];
@ -1576,6 +1590,18 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return item_list;
}
items_delete() {
this.update_localstorage_scanned_data();
}
update_localstorage_scanned_data() {
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.update_localstorage_scanned_data();
}
}
_set_values_for_item_list(children) {
const items_rule_dict = {};

View File

@ -865,16 +865,20 @@ erpnext.utils.map_current_doc = function(opts) {
}
if (opts.source_doctype) {
let data_fields = [];
if(opts.source_doctype == "Purchase Receipt") {
data_fields.push({
fieldname: 'merge_taxes',
fieldtype: 'Check',
label: __('Merge taxes from multiple documents'),
});
}
const d = new frappe.ui.form.MultiSelectDialog({
doctype: opts.source_doctype,
target: opts.target,
date_field: opts.date_field || undefined,
setters: opts.setters,
data_fields: [{
fieldname: 'merge_taxes',
fieldtype: 'Check',
label: __('Merge taxes from multiple documents'),
}],
data_fields: data_fields,
get_query: opts.get_query,
add_filters_group: 1,
allow_child_item_selection: opts.allow_child_item_selection,
@ -888,7 +892,10 @@ erpnext.utils.map_current_doc = function(opts) {
return;
}
opts.source_name = values;
opts.args = args;
if (opts.allow_child_item_selection || opts.source_doctype == "Purchase Receipt") {
// args contains filtered child docnames
opts.args = args;
}
d.dialog.hide();
_map();
},

View File

@ -7,8 +7,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];
this.barcode_field = opts.barcode_field || "barcode";
this.serial_no_field = opts.serial_no_field || "serial_no";
this.batch_no_field = opts.batch_no_field || "batch_no";
this.uom_field = opts.uom_field || "uom";
this.qty_field = opts.qty_field || "qty";
// field name on row which defines max quantity to be scanned e.g. picklist
@ -84,6 +82,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
update_table(data) {
return new Promise((resolve, reject) => {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
frappe.flags.trigger_from_barcode_scanner = true;
const {item_code, barcode, batch_no, serial_no, uom} = data;
@ -106,50 +105,38 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.frm.has_items = false;
}
if (this.is_duplicate_serial_no(row, serial_no)) {
if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) {
this.clean_up();
reject();
return;
}
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
() => this.set_barcode(row, barcode),
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
this.show_scan_message(row.idx, row.item_code, qty);
}),
() => this.set_barcode_uom(row, uom),
() => this.clean_up(),
() => this.revert_selector_flag(),
() => resolve(row)
() => resolve(row),
() => {
if (row.serial_and_batch_bundle && !this.frm.is_new()) {
this.frm.save();
}
frappe.flags.trigger_from_barcode_scanner = false;
}
]);
});
}
// batch and serial selector is reduandant when all info can be added by scan
// this flag on item row is used by transaction.js to avoid triggering selector
set_selector_trigger_flag(data) {
const {has_batch_no, has_serial_no} = data;
const require_selecting_batch = has_batch_no;
const require_selecting_serial = has_serial_no;
if (!(require_selecting_batch || require_selecting_serial)) {
frappe.flags.hide_serial_batch_dialog = true;
}
}
revert_selector_flag() {
frappe.flags.hide_serial_batch_dialog = false;
}
set_item(row, item_code, barcode, batch_no, serial_no) {
return new Promise(resolve => {
const increment = async (value = 1) => {
const item_data = {item_code: item_code};
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
frappe.flags.trigger_from_barcode_scanner = true;
await frappe.model.set_value(row.doctype, row.name, item_data);
return value;
};
@ -158,8 +145,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
increment(value).then((value) => resolve(value));
});
} else if (this.frm.has_items) {
this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no);
} else {
increment().then((value) => resolve(value));
}
@ -182,9 +167,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.model.set_value(row.doctype, row.name, item_data);
frappe.run_serially([
() => this.set_batch_no(row, this.dialog.get_value("batch_no")),
() => this.set_barcode(row, this.dialog.get_value("barcode")),
() => this.set_serial_no(row, this.dialog.get_value("serial_no")),
() => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")),
() => this.add_child_for_remaining_qty(row),
() => this.clean_up()
]);
@ -338,32 +322,144 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
async set_serial_no(row, serial_no) {
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
const existing_serial_nos = row[this.serial_no_field];
let new_serial_nos = "";
if (!!existing_serial_nos) {
new_serial_nos = existing_serial_nos + "\n" + serial_no;
} else {
new_serial_nos = serial_no;
}
await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
async set_serial_and_batch(row, item_code, serial_no, batch_no) {
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no);
} else if(row.serial_and_batch_bundle) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch",
args: {
bundle_id: row.serial_and_batch_bundle,
serial_no: serial_no,
batch_no: batch_no,
},
})
}
}
get_key_for_localstorage() {
let parts = this.frm.doc.name.split("-");
return parts[parts.length - 1] + this.frm.doc.doctype;
}
update_localstorage_scanned_data() {
let docname = this.frm.doc.name
if (localStorage[docname]) {
let items = JSON.parse(localStorage[docname]);
let existing_items = this.frm.doc.items.map(d => d.item_code);
if (!existing_items.length) {
localStorage.removeItem(docname);
return;
}
for (let item_code in items) {
if (!existing_items.includes(item_code)) {
delete items[item_code];
}
}
localStorage[docname] = JSON.stringify(items);
}
}
async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) {
let docname = this.frm.doc.name
let entries = JSON.parse(localStorage.getItem(docname));
if (!entries) {
entries = {};
}
let key = item_code;
if (!entries[key]) {
entries[key] = [];
}
let existing_row = [];
if (!serial_no && batch_no) {
existing_row = entries[key].filter((e) => e.batch_no === batch_no);
if (existing_row.length) {
existing_row[0].qty += 1;
}
} else if (serial_no) {
existing_row = entries[key].filter((e) => e.serial_no === serial_no);
if (existing_row.length) {
frappe.throw(__("Serial No {0} has already scanned.", [serial_no]));
}
}
if (!existing_row.length) {
entries[key].push({
"serial_no": serial_no,
"batch_no": batch_no,
"qty": 1
});
}
localStorage.setItem(docname, JSON.stringify(entries));
// Auto remove from localstorage after 1 hour
setTimeout(() => {
localStorage.removeItem(docname);
}, 3600000)
}
remove_item_from_localstorage() {
let docname = this.frm.doc.name;
if (localStorage[docname]) {
localStorage.removeItem(docname);
}
}
async sync_bundle_data() {
let docname = this.frm.doc.name;
if (localStorage[docname]) {
let entries = JSON.parse(localStorage[docname]);
if (entries) {
for (let entry in entries) {
let row = this.frm.doc.items.filter((item) => {
if (item.item_code === entry) {
return true;
}
})[0];
if (row) {
this.create_serial_and_batch_bundle(row, entries, entry)
.then(() => {
if (!entries) {
localStorage.removeItem(docname);
}
});
}
}
}
}
}
async create_serial_and_batch_bundle(row, entries, key) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
args: {
entries: entries[key],
child_row: row,
doc: this.frm.doc,
warehouse: row.warehouse,
do_not_save: 1
},
callback: function(r) {
row.serial_and_batch_bundle = r.message.name;
delete entries[key];
}
})
}
async set_barcode_uom(row, uom) {
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
}
}
async set_batch_no(row, batch_no) {
if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
}
}
async set_barcode(row, barcode) {
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
@ -379,13 +475,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
is_duplicate_serial_no(row, serial_no) {
const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
is_duplicate_serial_no(row, item_code, serial_no) {
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
let is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
if (is_duplicate) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
if (is_duplicate) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
return is_duplicate;
} else if (row.serial_and_batch_bundle) {
this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
if (r.message) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
return r.message;
})
}
return is_duplicate;
}
async check_duplicate_serial_no_in_db(row, serial_no, response) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
args: {
serial_no: serial_no,
bundle_id: row.serial_and_batch_bundle
},
callback(r) {
response(r);
}
})
}
check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
let docname = this.frm.doc.name
let entries = JSON.parse(localStorage.getItem(docname));
if (!entries) {
return false;
}
let existing_row = [];
if (entries[item_code]) {
existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
}
return existing_row.length;
}
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {

View File

@ -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);
}

View File

@ -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()

View File

@ -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,
},

View File

@ -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):

View File

@ -9,7 +9,7 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.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,

View File

@ -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 = []

View File

@ -149,6 +149,11 @@ def convert_order_to_invoices():
invoice.set_posting_time = 1
invoice.posting_date = order.transaction_date
invoice.due_date = order.transaction_date
invoice.bill_date = order.transaction_date
if invoice.get("payment_schedule"):
invoice.payment_schedule[0].due_date = order.transaction_date
invoice.update_stock = 1
invoice.submit()

View File

@ -249,7 +249,7 @@ class Company(NestedSet):
if frappe.flags.parent_company_changed:
from frappe.utils.nestedset import rebuild_tree
rebuild_tree("Company", "parent_company")
rebuild_tree("Company")
frappe.clear_cache()
@ -397,7 +397,7 @@ class Company(NestedSet):
frappe.local.flags.ignore_update_nsm = True
make_records(records)
frappe.local.flags.ignore_update_nsm = False
rebuild_tree("Department", "parent_department")
rebuild_tree("Department")
def validate_coa_input(self):
if self.create_chart_of_accounts_based_on == "Existing Company":

View File

@ -616,8 +616,8 @@
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date",
"no_copy": 1,
"mandatory_depends_on": "eval:doc.status == \"Left\"",
"no_copy": 1,
"oldfieldname": "relieving_date",
"oldfieldtype": "Date"
},
@ -822,12 +822,14 @@
"icon": "fa fa-user",
"idx": 24,
"image_field": "image",
"is_tree": 1,
"links": [],
"modified": "2023-10-04 10:57:05.174592",
"modified": "2024-01-03 17:36:20.984421",
"modified_by": "Administrator",
"module": "Setup",
"name": "Employee",
"naming_rule": "By \"Naming Series\" field",
"nsm_parent_field": "reports_to",
"owner": "Administrator",
"permissions": [
{
@ -860,7 +862,6 @@
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
}
@ -871,4 +872,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "employee_name"
}
}

View File

@ -79,7 +79,7 @@ class TestItem(unittest.TestCase):
group_b.save()
def test_rebuild_tree(self):
rebuild_tree("Item Group", "parent_item_group")
rebuild_tree("Item Group")
self.test_basic_tree()
def move_it_back(self):

View File

@ -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)

View File

@ -59,7 +59,7 @@ def get_warehouse_account(warehouse, warehouse_account=None):
else:
from frappe.utils.nestedset import rebuild_tree
rebuild_tree("Warehouse", "parent_warehouse")
rebuild_tree("Warehouse")
else:
account = frappe.db.sql(
"""

View File

@ -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)
)
)
)

View File

@ -10,6 +10,7 @@ from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.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,

View File

@ -305,7 +305,7 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None):
dimensions = get_document_wise_inventory_dimensions(doc.doctype)
filter_dimensions = []
for row in dimensions:
if row.type_of_transaction:
if row.type_of_transaction and row.type_of_transaction != "Both":
if (
row.type_of_transaction == "Inward"
if doc.docstatus == 1

View File

@ -429,6 +429,14 @@ class TestInventoryDimension(FrappeTestCase):
)
warehouse = create_warehouse("Negative Stock Warehouse")
doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True)
doc.items[0].inv_site = "Site 1"
self.assertRaises(frappe.ValidationError, doc.submit)
doc.reload()
if doc.docstatus == 1:
doc.cancel()
doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
doc.items[0].to_inv_site = "Site 1"

View File

@ -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
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -302,7 +302,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))

View File

@ -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)

View File

@ -140,11 +140,8 @@ 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.db.get_single_value(
"Accounts Settings", ["acc_frozen_upto", "frozen_accounts_modifier"], as_dict=1
)
if not acc_settings.acc_frozen_upto:
return

View File

@ -729,19 +729,13 @@ class SerialandBatchBundle(Document):
def before_cancel(self):
self.delink_serial_and_batch_bundle()
self.clear_table()
def delink_serial_and_batch_bundle(self):
self.voucher_no = None
sles = frappe.get_all("Stock Ledger Entry", filters={"serial_and_batch_bundle": self.name})
for sle in sles:
frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_and_batch_bundle", None)
def clear_table(self):
self.set("entries", [])
@property
def child_table(self):
if self.voucher_type == "Job Card":
@ -876,7 +870,6 @@ class SerialandBatchBundle(Document):
self.validate_voucher_no_docstatus()
self.delink_refernce_from_voucher()
self.delink_reference_from_batch()
self.clear_table()
@frappe.whitelist()
def add_serial_batch(self, data):
@ -1011,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,
@ -1053,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:
@ -1156,7 +1160,7 @@ def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name
@frappe.whitelist()
def add_serial_batch_ledgers(entries, child_row, doc, warehouse) -> object:
def add_serial_batch_ledgers(entries, child_row, doc, warehouse, do_not_save=False) -> object:
if isinstance(child_row, str):
child_row = frappe._dict(parse_json(child_row))
@ -1170,20 +1174,23 @@ def add_serial_batch_ledgers(entries, child_row, doc, warehouse) -> object:
if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
sb_doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
else:
sb_doc = create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
sb_doc = create_serial_batch_no_ledgers(
entries, child_row, parent_doc, warehouse, do_not_save=do_not_save
)
return sb_doc
def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
def create_serial_batch_no_ledgers(
entries, child_row, parent_doc, warehouse=None, do_not_save=False
) -> object:
warehouse = warehouse or (
child_row.rejected_warehouse if child_row.is_rejected else child_row.warehouse
)
type_of_transaction = child_row.type_of_transaction
type_of_transaction = get_type_of_transaction(parent_doc, child_row)
if parent_doc.get("doctype") == "Stock Entry":
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse
doc = frappe.get_doc(
@ -1214,13 +1221,30 @@ def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
doc.save()
frappe.db.set_value(child_row.doctype, child_row.name, "serial_and_batch_bundle", doc.name)
if do_not_save:
frappe.db.set_value(child_row.doctype, child_row.name, "serial_and_batch_bundle", doc.name)
frappe.msgprint(_("Serial and Batch Bundle created"), alert=True)
return doc
def get_type_of_transaction(parent_doc, child_row):
type_of_transaction = child_row.type_of_transaction
if parent_doc.get("doctype") == "Stock Entry":
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
if not type_of_transaction:
type_of_transaction = "Outward"
if parent_doc.get("doctype") in ["Purchase Receipt", "Purchase Invoice"]:
type_of_transaction = "Inward"
if parent_doc.get("is_return"):
type_of_transaction = "Inward" if type_of_transaction == "Outward" else "Outward"
return type_of_transaction
def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
doc.voucher_detail_no = child_row.name
@ -1247,6 +1271,25 @@ def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
return doc
@frappe.whitelist()
def update_serial_or_batch(bundle_id, serial_no=None, batch_no=None):
if batch_no and not serial_no:
if qty := frappe.db.get_value(
"Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty"
):
frappe.db.set_value(
"Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty", qty + 1
)
return
doc = frappe.get_cached_doc("Serial and Batch Bundle", bundle_id)
if not serial_no and not batch_no:
return
doc.append("entries", {"serial_no": serial_no, "batch_no": batch_no, "qty": 1})
doc.save(ignore_permissions=True)
def get_serial_and_batch_ledger(**kwargs):
kwargs = frappe._dict(kwargs)
@ -2032,3 +2075,8 @@ def get_stock_ledgers_batches(kwargs):
@frappe.whitelist()
def get_batch_no_from_serial_no(serial_no):
return frappe.get_cached_value("Serial No", serial_no, "batch_no")
@frappe.whitelist()
def is_duplicate_serial_no(bundle_id, serial_no):
return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no})

View File

@ -10,6 +10,8 @@ from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
add_serial_batch_ledgers,
make_batch_nos,
make_serial_nos,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@ -481,6 +483,38 @@ class TestSerialandBatchBundle(FrappeTestCase):
docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
self.assertEqual(docstatus, 2)
def test_batch_duplicate_entry(self):
item_code = make_item(properties={"has_batch_no": 1}).name
batch_id = "TEST-BATTCCH-VAL-00001"
batch_nos = [{"batch_no": batch_id, "qty": 1}]
make_batch_nos(item_code, batch_nos)
self.assertTrue(frappe.db.exists("Batch", batch_id))
batch_id = "TEST-BATTCCH-VAL-00001"
batch_nos = [{"batch_no": batch_id, "qty": 1}]
# Shouldn't throw duplicate entry error
make_batch_nos(item_code, batch_nos)
self.assertTrue(frappe.db.exists("Batch", batch_id))
def test_serial_no_duplicate_entry(self):
item_code = make_item(properties={"has_serial_no": 1}).name
serial_no_id = "TEST-SNID-VAL-00001"
serial_nos = [{"serial_no": serial_no_id, "qty": 1}]
make_serial_nos(item_code, serial_nos)
self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
serial_no_id = "TEST-SNID-VAL-00001"
serial_nos = [{"batch_no": serial_no_id, "qty": 1}]
# Shouldn't throw duplicate entry error
make_serial_nos(item_code, serial_nos)
self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos

View File

@ -24,6 +24,7 @@ from frappe.utils import (
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import (
add_additional_cost,
@ -208,7 +209,6 @@ class StockEntry(StockController):
self.validate_bom()
self.set_process_loss_qty()
self.validate_purchase_order()
self.validate_subcontracting_order()
if self.purpose in ("Manufacture", "Repack"):
self.mark_finished_and_scrap_items()
@ -274,6 +274,7 @@ class StockEntry(StockController):
return False
def on_submit(self):
self.validate_closed_subcontracting_order()
self.update_stock_ledger()
self.update_work_order()
self.validate_subcontract_order()
@ -294,6 +295,7 @@ class StockEntry(StockController):
self.set_material_request_transfer_status("Completed")
def on_cancel(self):
self.validate_closed_subcontracting_order()
self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status()
@ -393,9 +395,8 @@ class StockEntry(StockController):
frappe.delete_doc("Stock Entry", d.name)
def set_transfer_qty(self):
self.validate_qty_is_not_zero()
for item in self.get("items"):
if not flt(item.qty):
frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx), title=_("Zero quantity"))
if not flt(item.conversion_factor):
frappe.throw(_("Row {0}: UOM Conversion Factor is mandatory").format(item.idx))
item.transfer_qty = flt(
@ -1203,19 +1204,9 @@ class StockEntry(StockController):
)
)
def validate_subcontracting_order(self):
if self.get("subcontracting_order") and self.purpose in [
"Send to Subcontractor",
"Material Transfer",
]:
sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status")
if sco_status == "Closed":
frappe.throw(
_("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format(
self.subcontracting_order
)
)
def validate_closed_subcontracting_order(self):
if self.get("subcontracting_order"):
check_on_hold_or_closed_status("Subcontracting Order", self.subcontracting_order)
def mark_finished_and_scrap_items(self):
if self.purpose != "Repack" and any(

View File

@ -8,6 +8,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.stock.doctype.item.test_item import (
create_item,
make_item,
@ -54,6 +55,18 @@ class TestStockEntry(FrappeTestCase):
frappe.db.rollback()
frappe.set_user("Administrator")
def test_stock_entry_qty(self):
item_code = "_Test Item 2"
warehouse = "_Test Warehouse - _TC"
se = make_stock_entry(item_code=item_code, target=warehouse, qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
se.save()
# No error with qty=1
se.items[0].qty = 1
se.save()
self.assertEqual(se.items[0].qty, 1)
def test_fifo(self):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
item_code = "_Test Item 2"

View File

@ -111,16 +111,20 @@ class StockLedgerEntry(Document):
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"company": self.company,
"sle": self.name,
}
)
sle = get_previous_sle(kwargs, extra_cond=extra_cond)
qty_after_transaction = 0.0
flt_precision = cint(frappe.db.get_default("float_precision")) or 2
if sle:
flt_precision = cint(frappe.db.get_default("float_precision")) or 2
diff = sle.qty_after_transaction + flt(self.actual_qty)
diff = flt(diff, flt_precision)
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimensions)
qty_after_transaction = sle.qty_after_transaction
diff = qty_after_transaction + flt(self.actual_qty)
diff = flt(diff, flt_precision)
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimensions)
def throw_validation_error(self, diff, dimensions):
dimension_msg = _(", with the inventory {0}: {1}").format(

View File

@ -18,7 +18,7 @@ def reorder_item():
if not (frappe.db.a_row_exists("Company") and frappe.db.a_row_exists("Fiscal Year")):
return
if cint(frappe.db.get_value("Stock Settings", None, "auto_indent")):
if cint(frappe.db.get_single_value("Stock Settings", "auto_indent")):
return _reorder_item()
@ -212,7 +212,7 @@ def create_material_request(material_requests):
if mr_list:
if getattr(frappe.local, "reorder_email_notify", None) is None:
frappe.local.reorder_email_notify = cint(
frappe.db.get_value("Stock Settings", None, "reorder_email_notify")
frappe.db.get_single_value("Stock Settings", "reorder_email_notify")
)
if frappe.local.reorder_email_notify:

View File

@ -209,7 +209,7 @@ class SerialBatchBundle:
frappe.db.set_value(
"Serial and Batch Bundle",
{"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
{"is_cancelled": 1, "voucher_no": ""},
{"is_cancelled": 1},
)
if self.sle.serial_and_batch_bundle:

View File

@ -15,8 +15,8 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False,
frappe.db.auto_commit_on_many_writes = 1
if allow_negative_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)

View File

@ -338,7 +338,7 @@ def get_valuation_method(item_code):
val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True)
if not val_method:
val_method = (
frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO"
frappe.db.get_single_value("Stock Settings", "valuation_method", cache=True) or "FIFO"
)
return val_method
@ -591,6 +591,13 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
as_dict=True,
)
if batch_no_data:
if frappe.get_cached_value("Item", batch_no_data.item_code, "has_serial_no"):
frappe.throw(
_(
"Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead."
).format(search_value, batch_no_data.item_code)
)
_update_item_info(batch_no_data)
set_cache(batch_no_data)
return batch_no_data

View File

@ -101,9 +101,32 @@ frappe.ui.form.on('Subcontracting Order', {
},
refresh: function (frm) {
if (frm.doc.docstatus == 1 && frm.has_perm("submit")) {
if (frm.doc.status == "Closed") {
frm.add_custom_button(__('Re-open'), () => frm.events.update_subcontracting_order_status(frm), __("Status"));
} else if(flt(frm.doc.per_received, 2) < 100) {
frm.add_custom_button(__('Close'), () => frm.events.update_subcontracting_order_status(frm, "Closed"), __("Status"));
}
}
frm.trigger('get_materials_from_supplier');
},
update_subcontracting_order_status(frm, status) {
frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status",
args: {
sco: frm.doc.name,
status: status,
},
callback: function (r) {
if (!r.exc) {
frm.reload_doc();
}
},
});
},
get_materials_from_supplier: function (frm) {
let sco_rm_details = [];

View File

@ -370,7 +370,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled",
"options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled\nClosed",
"print_hide": 1,
"read_only": 1,
"reqd": 1,
@ -454,7 +454,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2023-06-03 16:18:17.782538",
"modified": "2024-01-03 20:56:04.670380",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order",

View File

@ -7,7 +7,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import get_bin
@ -68,6 +68,7 @@ class SubcontractingOrder(SubcontractingController):
"Material Transferred",
"Partial Material Transferred",
"Cancelled",
"Closed",
]
supplied_items: DF.Table[SubcontractingOrderSuppliedItem]
supplier: DF.Link
@ -112,16 +113,10 @@ class SubcontractingOrder(SubcontractingController):
def on_submit(self):
self.update_prevdoc_status()
self.update_requested_qty()
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
self.update_status()
def on_cancel(self):
self.update_prevdoc_status()
self.update_requested_qty()
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
self.update_status()
def validate_purchase_order_for_subcontracting(self):
@ -277,6 +272,9 @@ class SubcontractingOrder(SubcontractingController):
self.set_missing_values()
def update_status(self, status=None, update_modified=True):
if self.status == "Closed" and self.status != status:
check_on_hold_or_closed_status("Purchase Order", self.purchase_order)
if self.docstatus >= 1 and not status:
if self.docstatus == 1:
if self.status == "Draft":
@ -285,11 +283,6 @@ class SubcontractingOrder(SubcontractingController):
status = "Completed"
elif self.per_received > 0 and self.per_received < 100:
status = "Partially Received"
for item in self.supplied_items:
if not item.returned_qty or (item.supplied_qty - item.consumed_qty - item.returned_qty) > 0:
break
else:
status = "Closed"
else:
total_required_qty = total_supplied_qty = 0
for item in self.supplied_items:
@ -304,13 +297,12 @@ class SubcontractingOrder(SubcontractingController):
elif self.docstatus == 2:
status = "Cancelled"
if status:
frappe.db.set_value(
"Subcontracting Order", self.name, "status", status, update_modified=update_modified
)
if status and self.status != status:
self.db_set("status", status, update_modified=update_modified)
if status == "Closed":
update_po_status("Closed", self.purchase_order)
self.update_requested_qty()
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
@frappe.whitelist()
@ -357,8 +349,8 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None):
@frappe.whitelist()
def update_subcontracting_order_status(sco):
def update_subcontracting_order_status(sco, status=None):
if isinstance(sco, str):
sco = frappe.get_doc("Subcontracting Order", sco)
sco.update_status()
sco.update_status(status)

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