Compare commits

..

6 Commits

Author SHA1 Message Date
Frappe PR Bot
a5de60c357 chore(release): Bumped to Version 16.29.0
# [16.29.0](https://github.com/frappe/erpnext/compare/v16.28.0...v16.29.0) (2026-07-22)

### Bug Fixes

* add fetch from in production plan material request child table ([2852671](2852671cd5))
* added missing validations for `Dunning Type` (backport [#57224](https://github.com/frappe/erpnext/issues/57224)) ([#57227](https://github.com/frappe/erpnext/issues/57227)) ([3948951](3948951904))
* allow delivery when a batch is reserved across multiple sales orders (backport [#57169](https://github.com/frappe/erpnext/issues/57169)) ([56bbca0](56bbca0203))
* block changing Stock account type when stock ledger entries exist (backport [#57283](https://github.com/frappe/erpnext/issues/57283)) ([#57285](https://github.com/frappe/erpnext/issues/57285)) ([a26296c](a26296ca50))
* consider min order qty in the purchase/transfer flow of production plan (backport [#57204](https://github.com/frappe/erpnext/issues/57204)) ([cbd4f93](cbd4f93f3d))
* correct typo in allow_negative_stock parameter ([e2be05e](e2be05e48b))
* dont overwrite rate with 0 if not found ([aa08f75](aa08f753b4))
* exclude fully-delivered reservations from the conflict message ([555c607](555c607f2f))
* exclude transferred_qty from work order item to pick list item mapping ([c466b49](c466b49d09)), closes [#57253](https://github.com/frappe/erpnext/issues/57253) [#57236](https://github.com/frappe/erpnext/issues/57236) [#56596](https://github.com/frappe/erpnext/issues/56596)
* hide job card field in purchase order item ([20f6ac8](20f6ac81b9))
* keep Standard Cost stock value in step with the standard rate ([fe65882](fe65882e59))
* name every conflicting voucher in the reserved batch error ([839fd5e](839fd5e3b6))
* parallel reposting stalls between scheduler ticks (backport [#57220](https://github.com/frappe/erpnext/issues/57220)) ([#57248](https://github.com/frappe/erpnext/issues/57248)) ([a3bfded](a3bfdede06))
* pass ctx keys get_price_list_rate_for reads, skip rate update on insert ([c3aea9c](c3aea9ca9c))
* **payments:** ensure `payments` app installed on the site in `payment_app_import_guard` (backport [#57342](https://github.com/frappe/erpnext/issues/57342)) ([#57344](https://github.com/frappe/erpnext/issues/57344)) ([2c8c076](2c8c076f6e))
* permission issue (backport [#57112](https://github.com/frappe/erpnext/issues/57112)) (backport [#57142](https://github.com/frappe/erpnext/issues/57142)) ([#57184](https://github.com/frappe/erpnext/issues/57184)) ([0817d10](0817d1064c))
* project % complete field allowing modification when manual method (backport [#57274](https://github.com/frappe/erpnext/issues/57274)) ([#57276](https://github.com/frappe/erpnext/issues/57276)) ([0d53f1a](0d53f1adb7))
* **project:** improved access control for project users (backport [#56675](https://github.com/frappe/erpnext/issues/56675)) ([#57181](https://github.com/frappe/erpnext/issues/57181)) ([b1adec7](b1adec7e9e))
* **report:** handle nonetype error in timesheet billing summary grouping logic ([b7cf3bf](b7cf3bf641))
* rescale batch FIFO slot values on stock reconciliation ([6b3b03f](6b3b03fcd8))
* rescale stock ageing FIFO slot values on stock reconciliation ([c9394c0](c9394c030f))
* resolve float precision before streaming stock ledger entries ([0f25254](0f252542c3))
* restrict jinja globals in process statement of accounts templates (backport [#56458](https://github.com/frappe/erpnext/issues/56458)) ([#57232](https://github.com/frappe/erpnext/issues/57232)) ([5f6952b](5f6952b15c))
* revalue batch reco slots only when the entry covers the full batch ([a4bf506](a4bf50656a))
* round outstanding qty guard consistently with the conflict gate ([f3e1b3f](f3e1b3fca7))
* set correct currency in supplier quotation net rate field ([e24ef84](e24ef847dc))
* show transaction currency symbol in Payment Request schedule dialog and reference table (backport [#57050](https://github.com/frappe/erpnext/issues/57050)) ([#57312](https://github.com/frappe/erpnext/issues/57312)) ([429b58b](429b58b833))
* **stock:** correct stock ageing value for moving average and lifo items ([#56693](https://github.com/frappe/erpnext/issues/56693)) ([02549ef](02549ef8c4))
* strip account number when building account name in COA importer ([e1ebfa7](e1ebfa7163))
* sync process loss percentage when fg qty changes ([beeffee](beeffee8f9))
* sync translations from crowdin ([#57187](https://github.com/frappe/erpnext/issues/57187)) ([827831a](827831a247))
* use system float precision for batch qty comparison ([92217b2](92217b2c45))
* validate buying price list on material request and update item rates on change ([025b2f2](025b2f2922))

### Features

* add on hold status to project ([57a49ce](57a49ce168))
* recalculate valuation rate and stock value from Bin ([a38cbfc](a38cbfc88e))
* **stock:** automatically link portal users to their associated contact profiles for customers and suppliers ([134d63d](134d63de78))
* **stock:** expose all Bin qty fields in Stock Summary and Stock Projected Qty ([0bdf258](0bdf258888))
2026-07-22 00:17:03 +00:00
Diptanil Saha
264bfa188b Merge pull request #57324 from frappe/version-16-hotfix
chore: release v16
2026-07-22 05:45:21 +05:30
Frappe PR Bot
de591661b9 chore(release): Bumped to Version 16.28.0
# [16.28.0](https://github.com/frappe/erpnext/compare/v16.27.0...v16.28.0) (2026-07-15)

### Bug Fixes

* **accounts:** added permission checks on `get_account_balances_coa` (backport [#57107](https://github.com/frappe/erpnext/issues/57107)) ([#57122](https://github.com/frappe/erpnext/issues/57122)) ([e8a5325](e8a532587d))
* allow barcode scan to add and increment items in pick list ([00a646a](00a646ac25))
* apply precision fix inline for v16-hotfix, drop develop-only asset_service refactor ([d3a8e91](d3a8e91cda))
* **asset:** allow asset repair creation for fully depreciated assets ([8658039](8658039e9a))
* **budget-variance:** correct month shift in comparison chart ([703e9a7](703e9a728c))
* guard company logo lookup in default letterheads ([7b54314](7b543142a2))
* **journal-entry:** fetch outstanding on foreign currency ([ec782ee](ec782ee20d))
* **manufacturing:** preserve job card transferred quantity ([d7e9321](d7e9321746))
* match depreciation schedule rows at currency precision to avoid duplicate JEs ([a1fae95](a1fae959ed))
* prevent duplicate material request items in purchase order ([4d951c1](4d951c1cf8))
* set explicit table and logo widths in grey letterhead ([0d80fef](0d80fef3bf))
* **stock:** fix sqlparse token limit in get_bundle_wise_serial_nos ([5a99dd6](5a99dd6016))
* **stock:** set stock_uom on transferred Stock Reservation Entries ([5991ecf](5991ecfa3d))
* **stock:** show qty (company) and qty (warehouse) in sales transactions ([0d9ace7](0d9ace7ab8))
* **tnc:** `get_terms_and_conditions` render_template with `safe_exec` (backport [#56944](https://github.com/frappe/erpnext/issues/56944)) ([#56977](https://github.com/frappe/erpnext/issues/56977)) ([40c85a0](40c85a0087))

### Features

* **balance-sheet:** implement execute_synced_report with full parity to normal report ([636bcbe](636bcbedc0))
* **general-ledger:** implement execute_synced_report with full parity to normal report ([19ec095](19ec095ff8))
* **profit-and-loss:** implement execute_synced_report with full parity to normal report ([21ddb00](21ddb00e20))
* **trial-balance:** implement execute_duckdb with full parity to normal report ([228418b](228418b05f))
* weekly auto-repost of incorrect stock valuation entries (backport [#56637](https://github.com/frappe/erpnext/issues/56637)) ([#56700](https://github.com/frappe/erpnext/issues/56700)) ([bf1b7f2](bf1b7f2bea))
2026-07-15 02:24:51 +00:00
ruthra kumar
9a7e796fd2 Merge pull request #57161 from frappe/version-16-hotfix
chore: release v16
2026-07-15 06:17:14 +05:30
Frappe PR Bot
9d5c7605b8 chore(release): Bumped to Version 16.27.0
# [16.27.0](https://github.com/frappe/erpnext/compare/v16.26.2...v16.27.0) (2026-07-13)

### Bug Fixes

* **`frappe_crm_api`:** handle failure for `after_app_install` and `after_app_uninstall` ([eac3afc](eac3afcd88))
* **accounts:** clear clearance date when amending reconciled voucher ([6f28135](6f281356a6)), closes [#54909](https://github.com/frappe/erpnext/issues/54909)
* **accounts:** retain invoice table on opening invoice creation error (backport [#56353](https://github.com/frappe/erpnext/issues/56353)) ([#57058](https://github.com/frappe/erpnext/issues/57058)) ([4136ca5](4136ca55a7))
* add page numbers to print format footer ([0371e8e](0371e8eaf0))
* added permission checks on various whitelisted functions (backport [#56745](https://github.com/frappe/erpnext/issues/56745)) ([#56946](https://github.com/frappe/erpnext/issues/56946)) ([7ce1289](7ce1289c10))
* address review comments ([056195c](056195ce07))
* auto fetch serial no from previous operation output (backport to v16) ([#56861](https://github.com/frappe/erpnext/issues/56861)) ([17f2de4](17f2de42f3))
* **banking:** allow negative balance in bank statement import (backport [#56959](https://github.com/frappe/erpnext/issues/56959)) ([#57054](https://github.com/frappe/erpnext/issues/57054)) ([835e32c](835e32cec7))
* **banking:** handle blank password protected PDFs and negative amounts in CR/DR columns (backport [#56690](https://github.com/frappe/erpnext/issues/56690)) ([#56694](https://github.com/frappe/erpnext/issues/56694)) ([19c318d](19c318df68))
* **banking:** use custom renderer for translated strings and parser for rules (backport [#56643](https://github.com/frappe/erpnext/issues/56643)) ([#56647](https://github.com/frappe/erpnext/issues/56647)) ([2cb577b](2cb577b912))
* block serialized to non-serialized item change when SABB exists (backport [#56773](https://github.com/frappe/erpnext/issues/56773)) ([#56775](https://github.com/frappe/erpnext/issues/56775)) ([19d03fe](19d03fee46))
* clear stray permission message when item dashboard has no warehouse access ([a39753e](a39753ee08))
* **company:** ignore user permissions for link fields having link to `Account` and `Cost Center` (backport [#56748](https://github.com/frappe/erpnext/issues/56748)) ([#56795](https://github.com/frappe/erpnext/issues/56795)) ([003b655](003b6554c4))
* correct filter handling in Sales Person-wise Transaction Summary + tests (backport [#56783](https://github.com/frappe/erpnext/issues/56783)) ([#56908](https://github.com/frappe/erpnext/issues/56908)) ([24f0989](24f0989ac1))
* **crm_settings:** skip allowed users check when frappe crm is installed locally ([dfe4d5a](dfe4d5ab73))
* display outstanding amount using company default currency (backport [#56785](https://github.com/frappe/erpnext/issues/56785)) ([#57009](https://github.com/frappe/erpnext/issues/57009)) ([bdba4c8](bdba4c8091))
* don't attempt to create SABB for non-serialized / non-batch items (backport [#56627](https://github.com/frappe/erpnext/issues/56627)) ([#56803](https://github.com/frappe/erpnext/issues/56803)) ([8c56a5a](8c56a5ac0c))
* don't treat batch slot at FIFO queue head as qty slot ([20f6dd0](20f6dd0224))
* enforce read permission on ledger preview endpoints ([#57039](https://github.com/frappe/erpnext/issues/57039)) ([5642359](5642359c10))
* fetch payment entry reference amounts from invoice (backport [#56928](https://github.com/frappe/erpnext/issues/56928)) ([#57042](https://github.com/frappe/erpnext/issues/57042)) ([67c85ef](67c85ef0af))
* FIFO queue checks and incorrect entries filter in stock ledger reports ([3571512](3571512101)), closes [#43619](https://github.com/frappe/erpnext/issues/43619)
* **financial_statement:** render columnar financial statements instea… (backport [#56921](https://github.com/frappe/erpnext/issues/56921)) ([#57053](https://github.com/frappe/erpnext/issues/57053)) ([e6a6c13](e6a6c13355))
* for purchases do voucher based reposting (backport [#56601](https://github.com/frappe/erpnext/issues/56601)) ([#56608](https://github.com/frappe/erpnext/issues/56608)) ([e1e6176](e1e6176ddc))
* guard against missing DocType in onboarding steps patch ([#56804](https://github.com/frappe/erpnext/issues/56804)) ([f72289e](f72289e27c))
* **item-attribute:** clear attribute values when marking numeric ([374b340](374b340e73))
* **item:** error on uncommitted input and escape values in variant dialog ([04c834d](04c834d6a9))
* **item:** rework multiple variant dialog for large numeric ranges ([025d0cd](025d0cd7f3))
* make represents company field in purchase invoice ignore user permissions ([4aed77d](4aed77df0c))
* make trend report based-on and group-by column labels translatable ([b8199d8](b8199d88b6))
* map stock_qty in apply_price_list_on_item (backport [#56869](https://github.com/frappe/erpnext/issues/56869)) ([#57052](https://github.com/frappe/erpnext/issues/57052)) ([240fb2c](240fb2c4b8))
* partial delivery note against pick list (backport [#56985](https://github.com/frappe/erpnext/issues/56985)) ([#57006](https://github.com/frappe/erpnext/issues/57006)) ([20255a8](20255a8a7f))
* **patch:** moved create_company_custom_fields from `pre_model_sync` to `post_model_sync` (backport [#56962](https://github.com/frappe/erpnext/issues/56962)) ([#56965](https://github.com/frappe/erpnext/issues/56965)) ([14efd14](14efd14384))
* **payment reconciliation:** honour user permissions on accounting di… ([#56560](https://github.com/frappe/erpnext/issues/56560)) ([8eb92b8](8eb92b8b18))
* precision issue causing reconciliation error ([#54043](https://github.com/frappe/erpnext/issues/54043)) ([20df15b](20df15b3ac))
* prevent repeatable read related concurrency errors ([019b02a](019b02adcd))
* race condition and repeatable read in process pcv ([6d9f5fa](6d9f5fac76))
* recompute transferred qty before deciding work order status ([bc952f4](bc952f40d9))
* remove company default on cost center in stock entry detail ([a1fe6ce](a1fe6cee5d))
* remove incorrect Payable account_type from Customer Deposits in Philippines CoA (backport [#57018](https://github.com/frappe/erpnext/issues/57018)) ([#57061](https://github.com/frappe/erpnext/issues/57061)) ([edfbc71](edfbc71135))
* rename variant item_code/item_name when attribute abbreviation changes ([97cd1e7](97cd1e7144))
* render letter head footer in print formats ([fa4d32d](fa4d32dcdb))
* replay immutable SLE qty for serial/batch bundle valuation (backport [#56814](https://github.com/frappe/erpnext/issues/56814)) ([#56835](https://github.com/frappe/erpnext/issues/56835)) ([2264e25](2264e25cc6))
* resolve conflicts ([3aad7fe](3aad7fee24))
* restore Save button on reverse journal entry (backport [#56770](https://github.com/frappe/erpnext/issues/56770)) ([#56772](https://github.com/frappe/erpnext/issues/56772)) ([4573cd1](4573cd15a9))
* **selling:** update sales order per billed on credit note submission ([224cf19](224cf19f33))
* set conversion_rate on quotation created from customer ([26905bc](26905bc142))
* show only template items in Variant Of filter ([bebe011](bebe011636))
* skip item prices tab render for users without item price read access ([c964811](c964811293))
* skip stock reservation for opted-out production plans ([17598e2](17598e2626)), closes [#56798](https://github.com/frappe/erpnext/issues/56798)
* **stock:** backfill transferred qty for existing pick lists ([903d78c](903d78cc43))
* **stock:** link job card in stock entry created from pick list (backport [#57031](https://github.com/frappe/erpnext/issues/57031)) ([755b9cc](755b9ccbc3))
* **stock:** pick list serial batch posting date ([#57015](https://github.com/frappe/erpnext/issues/57015)) ([7e46be2](7e46be2a33)), closes [#56951](https://github.com/frappe/erpnext/issues/56951)
* **stock:** propagate project from job card to stock entry ([51d2077](51d2077eed))
* **stock:** support quality inspection for stock entry by purpose (backport [#56446](https://github.com/frappe/erpnext/issues/56446)) ([40ca3b5](40ca3b5e5d))
* update BOM operations when routing is changed ([b2e6a39](b2e6a39743))
* update events order by date asc (backport [#56963](https://github.com/frappe/erpnext/issues/56963)) ([#57056](https://github.com/frappe/erpnext/issues/57056)) ([e2fd061](e2fd061b3d))
* update modified timestamp in item.json ([f602ee0](f602ee0e73))
* use live source warehouse valuation for internal transfer purchase receipts (backport [#56431](https://github.com/frappe/erpnext/issues/56431)) ([#56677](https://github.com/frappe/erpnext/issues/56677)) ([1da28f2](1da28f2278))
* validate planned end date is not before planned start date in work order ([745baad](745baad0d1))
* validate reverse GL entries on current date under immutable ledger (backport [#56709](https://github.com/frappe/erpnext/issues/56709)) ([#56752](https://github.com/frappe/erpnext/issues/56752)) ([eea4756](eea4756ce9))
* validate template and its variant in the same Pricing Rule ([159a253](159a2538da))

### Features

* capture user persona during setup (backport [#56705](https://github.com/frappe/erpnext/issues/56705)) ([#56707](https://github.com/frappe/erpnext/issues/56707)) ([d9aa449](d9aa4491f1))
* **crm_settings:** auto-update crm sync settings on frappe crm install and uninstall ([cb2a930](cb2a930a32))
* explain FIFO allocation of fixed Discount Amount on Sales Order ([#56436](https://github.com/frappe/erpnext/issues/56436)) ([62fed1d](62fed1d562))
* **manufacturing:** create material request for raw materials from work order ([#56980](https://github.com/frappe/erpnext/issues/56980)) ([144f92d](144f92d58e))
* **sla:** filter service level agreement link by document type (backport [#56954](https://github.com/frappe/erpnext/issues/56954)) ([#56956](https://github.com/frappe/erpnext/issues/56956)) ([393b5d1](393b5d1f74))
* **stock:** support partial transfer from pick list ([af495ed](af495ed253))

### Performance Improvements

* avoid per-row Warehouse doc fetches in auto reorder job ([39b5e12](39b5e12305))
* batch bin lookups in delivery note stock update ([697e2c0](697e2c0b66))
2026-07-13 07:41:31 +00:00
Diptanil Saha
f94eee3197 Merge pull request #56931 from frappe/version-16-hotfix
chore: release v16
2026-07-13 12:09:52 +05:30
38 changed files with 488 additions and 1163 deletions

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.26.2"
__version__ = "16.29.0"
def get_default_company(user=None):

View File

@@ -17,7 +17,7 @@ class ERPNextAddress(Address):
def link_address(self):
"""Link address based on owner"""
if self.get("is_your_company_address"):
if self.is_your_company_address:
return
return super().link_address()
@@ -28,9 +28,7 @@ class ERPNextAddress(Address):
self.is_your_company_address = 1
def validate_reference(self):
if self.get("is_your_company_address") and not [
row for row in self.links if row.link_doctype == "Company"
]:
if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]:
frappe.throw(
_(
"Address needs to be linked to a Company. Please add a row for Company in the Links table."

View File

@@ -75,8 +75,6 @@
"over_billing_allowance",
"credit_controller",
"role_allowed_to_over_bill",
"enable_overdue_billing_threshold",
"role_allowed_to_bypass_overdue_billing",
"column_break_11",
"assets_tab",
"asset_settings_section",
@@ -273,21 +271,6 @@
"label": "Role Allowed to over bill ",
"options": "Role"
},
{
"default": "0",
"description": "Block a new Sales Invoice when the customer's overdue amount exceeds the Overdue Limit set on the customer.",
"fieldname": "enable_overdue_billing_threshold",
"fieldtype": "Check",
"label": "Restrict Customer Over Billing"
},
{
"depends_on": "eval:doc.enable_overdue_billing_threshold",
"description": "Users with this role can still submit invoices for customers who have crossed their Overdue Limit.",
"fieldname": "role_allowed_to_bypass_overdue_billing",
"fieldtype": "Link",
"label": "Role Allowed to Bypass Over Billing Restriction",
"options": "Role"
},
{
"fieldname": "period_closing_settings_section",
"fieldtype": "Section Break"

View File

@@ -77,7 +77,6 @@ class AccountsSettings(Document):
enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_overdue_billing_threshold: DF.Check
enable_party_matching: DF.Check
enable_subscription: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
@@ -96,7 +95,6 @@ class AccountsSettings(Document):
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
repost_allowed_types: DF.Table[RepostAllowedTypes]
role_allowed_to_bypass_overdue_billing: DF.Link | None
role_allowed_to_over_bill: DF.Link | None
role_to_notify_on_depreciation_failure: DF.Link | None
role_to_override_stop_action: DF.Link | None
@@ -152,10 +150,6 @@ class AccountsSettings(Document):
toggle_subscription_sections(not self.enable_subscription)
clear_cache = True
if old_doc.enable_overdue_billing_threshold != self.enable_overdue_billing_threshold:
toggle_overdue_billing_threshold_field(not self.enable_overdue_billing_threshold)
clear_cache = True
if clear_cache:
frappe.clear_cache()
@@ -247,10 +241,6 @@ def toggle_subscription_sections(hide):
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
def toggle_overdue_billing_threshold_field(hide):
create_property_setter_for_hiding_field("Customer Credit Limit", "overdue_billing_threshold", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,

View File

@@ -517,7 +517,6 @@ class SalesInvoice(SellingController):
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit()
self.check_overdue_billing_threshold()
if cint(self.is_pos) != 1 and not self.is_return:
self.update_against_document_in_jv()
@@ -779,11 +778,6 @@ class SalesInvoice(SellingController):
pos_invoice_doc = frappe.get_doc("POS Invoice", pos_invoice)
pos_invoice_doc.cancel()
def check_overdue_billing_threshold(self):
from erpnext.selling.doctype.customer.customer import check_overdue_billing_threshold
check_overdue_billing_threshold(self.customer, self.company)
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)

View File

@@ -82,7 +82,8 @@
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
"options": "Cost Center",
"reqd": 1
},
{
"fieldname": "shipping_amount_section",
@@ -140,20 +141,19 @@
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"icon": "fa fa-truck",
"idx": 1,
"links": [],
"modified": "2026-07-22 14:53:27.315435",
"modified": "2024-03-27 13:10:41.653314",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Shipping Rule",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -197,8 +197,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

View File

@@ -36,17 +36,18 @@ class ShippingRule(Document):
from erpnext.accounts.doctype.shipping_rule_condition.shipping_rule_condition import (
ShippingRuleCondition,
)
from erpnext.accounts.doctype.shipping_rule_country.shipping_rule_country import ShippingRuleCountry
from erpnext.accounts.doctype.shipping_rule_country.shipping_rule_country import (
ShippingRuleCountry,
)
account: DF.Link
calculate_based_on: DF.Literal["Fixed", "Net Total", "Net Weight"]
company: DF.Link
conditions: DF.Table[ShippingRuleCondition]
cost_center: DF.Link | None
cost_center: DF.Link
countries: DF.Table[ShippingRuleCountry]
disabled: DF.Check
label: DF.Data
project: DF.Link | None
shipping_amount: DF.Currency
shipping_rule_type: DF.Literal["Selling", "Buying"]
# end: auto-generated types

View File

@@ -858,7 +858,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
companies = frappe.get_list(
companies = frappe.get_all(
doctype, filters={"docstatus": 1, party_type.lower(): party}, distinct=1, fields=["company"]
)

View File

@@ -13,7 +13,7 @@ frappe.query_reports["Accounts Payable"] = {
},
{
fieldname: "report_date",
label: __("Report Date"),
label: __("Posting Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
@@ -69,10 +69,10 @@ frappe.query_reports["Accounts Payable"] = {
default: "Due Date",
},
{
fieldname: "age_as_on",
label: __("Age as on"),
fieldname: "calculate_ageing_with",
label: __("Calculate Ageing With"),
fieldtype: "Select",
options: "Report Date\nToday",
options: "Report Date\nToday Date",
default: "Report Date",
},
{

View File

@@ -12,7 +12,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
{
fieldname: "report_date",
label: __("Report Date"),
label: __("Posting Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
@@ -24,10 +24,10 @@ frappe.query_reports["Accounts Payable Summary"] = {
default: "Due Date",
},
{
fieldname: "age_as_on",
label: __("Age as on"),
fieldname: "calculate_ageing_with",
label: __("Calculate Ageing With"),
fieldtype: "Select",
options: "Report Date\nToday",
options: "Report Date\nToday Date",
default: "Report Date",
},
{

View File

@@ -15,7 +15,7 @@ frappe.query_reports["Accounts Receivable"] = {
},
{
fieldname: "report_date",
label: __("Report Date"),
label: __("Posting Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
@@ -98,10 +98,10 @@ frappe.query_reports["Accounts Receivable"] = {
default: "Due Date",
},
{
fieldname: "age_as_on",
label: __("Age as on"),
fieldname: "calculate_ageing_with",
label: __("Calculate Ageing With"),
fieldtype: "Select",
options: "Report Date\nToday",
options: "Report Date\nToday Date",
default: "Report Date",
},
{

View File

@@ -55,7 +55,8 @@ class ReceivablePayableReport:
self.filters.report_date = getdate(self.filters.report_date or nowdate())
self.age_as_on = (
getdate(nowdate())
if "age_as_on" not in self.filters or self.filters.age_as_on == "Today"
if "calculate_ageing_with" not in self.filters
or self.filters.calculate_ageing_with == "Today Date"
else self.filters.report_date
)

View File

@@ -12,7 +12,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
{
fieldname: "report_date",
label: __("Report Date"),
label: __("Posting Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
@@ -24,10 +24,10 @@ frappe.query_reports["Accounts Receivable Summary"] = {
default: "Due Date",
},
{
fieldname: "age_as_on",
label: __("Age as on"),
fieldname: "calculate_ageing_with",
label: __("Calculate Ageing With"),
fieldtype: "Select",
options: "Report Date\nToday",
options: "Report Date\nToday Date",
default: "Report Date",
},
{

View File

@@ -1371,9 +1371,8 @@ class StockController(AccountsController):
if outstanding > 0:
reservations[key].append(row)
precision = frappe.get_precision("Serial and Batch Entry", "qty")
for (batch_no, warehouse), reserved_qty in outstanding_qty.items():
if flt(reserved_qty, precision) <= 0:
if flt(reserved_qty, 6) <= 0:
continue
batch_qty = get_batch_qty(
@@ -1384,7 +1383,7 @@ class StockController(AccountsController):
consider_negative_batches=True,
)
if flt(batch_qty, precision) >= flt(reserved_qty, precision):
if flt(batch_qty, 6) >= flt(reserved_qty, 6):
continue
vouchers = ", ".join(

File diff suppressed because it is too large Load Diff

View File

@@ -79,11 +79,6 @@ frappe.ui.form.on("BOM Operation", {
const d = locals[cdt][cdn];
frm.events.calculate_operating_cost(frm, d);
},
hour_rate: function (frm, cdt, cdn) {
const d = locals[cdt][cdn];
frm.events.calculate_operating_cost(frm, d);
},
});
frappe.tour["Routing"] = [

View File

@@ -2554,13 +2554,7 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True):
@frappe.whitelist()
def make_work_order(
bom_no: str,
item: str,
qty: float = 0,
company: str | None = None,
project: str | None = None,
variant_items: str | list | None = None,
use_multi_level_bom: bool | None = None,
bom_no, item, qty=0, company=None, project=None, variant_items=None, use_multi_level_bom=None
):
from erpnext import get_default_company
@@ -2569,8 +2563,7 @@ def make_work_order(
item_details = get_item_details(item, project)
# selected BOM already belongs to this variant — keep it
if frappe.db.get_value("Item", item, "variant_of") and frappe.db.get_value("BOM", bom_no, "item") != item:
if frappe.db.get_value("Item", item, "variant_of"):
if variant_bom := frappe.db.get_value(
"BOM",
{"item": item, "is_default": 1, "docstatus": 1},

View File

@@ -491,4 +491,3 @@ erpnext.patches.v15_0.backfill_sla_link_filters_on_docfield
erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm
erpnext.patches.v16_0.backfill_pick_list_transferred_qty
erpnext.patches.v16_0.access_control_for_project_users
erpnext.patches.v16_0.rename_ar_ap_ageing_filter

View File

@@ -1,45 +0,0 @@
import frappe
REPORTS = (
"Accounts Receivable",
"Accounts Payable",
"Accounts Receivable Summary",
"Accounts Payable Summary",
)
def execute():
# filter `calculate_ageing_with` -> `age_as_on`, option "Today Date" -> "Today"
_migrate("Auto Email Report", "filters", "report")
_migrate("Dashboard Chart", "filters_json", "report_name", type_field="chart_type")
_migrate("Number Card", "filters_json", "report_name", type_field="type")
def _migrate(doctype, filter_field, report_field, type_field=None):
conditions = {report_field: ("in", REPORTS)}
if type_field:
conditions[type_field] = "Report"
for row in frappe.get_all(doctype, filters=conditions, fields=["name", filter_field]):
updated = _rewrite(row.get(filter_field))
if updated is not None:
frappe.db.set_value(doctype, row.name, filter_field, updated, update_modified=False)
def _rewrite(raw):
if not raw:
return None
try:
filters = frappe.parse_json(raw)
except ValueError:
return None
if not isinstance(filters, dict) or "calculate_ageing_with" not in filters:
return None
filters["age_as_on"] = filters.pop("calculate_ageing_with")
if filters["age_as_on"] == "Today Date":
filters["age_as_on"] = "Today"
return frappe.as_json(filters, indent=None)

View File

@@ -11,36 +11,6 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
}
}
onload_post_render() {
this.set_route_options_for_new_doc();
}
set_route_options_for_new_doc() {
// While creating a Batch or Serial and Batch Bundle from the link
// field, copy details from the line item to the new form
if (!this.frm.fields_dict.items) return;
let batch_no_field = this.frm.get_docfield("items", "batch_no");
if (batch_no_field) {
batch_no_field.get_route_options_for_new_doc = (row) => {
return {
item: row.doc.item_code,
};
};
}
let sbb_field = this.frm.get_docfield("items", "serial_and_batch_bundle");
if (sbb_field) {
sbb_field.get_route_options_for_new_doc = (row) => {
return {
item_code: row.doc.item_code,
warehouse: row.doc.warehouse || row.doc.s_warehouse || row.doc.t_warehouse,
voucher_type: this.frm.doc.doctype,
};
};
}
}
barcode(doc, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.barcode) {

View File

@@ -649,6 +649,34 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
erpnext.toggle_serial_batch_fields(this.frm);
}
set_route_options_for_new_doc() {
// While creating the batch from the link field, copy item from line item to batch form
if (this.frm.fields_dict["items"].grid.get_field("batch_no")) {
let batch_no_field = this.frm.get_docfield("items", "batch_no");
if (batch_no_field) {
batch_no_field.get_route_options_for_new_doc = function (row) {
return {
item: row.doc.item_code,
};
};
}
}
// While creating the SABB from the link field, copy item, doctype from line item to SABB form
if (this.frm.fields_dict["items"].grid.get_field("serial_and_batch_bundle")) {
let sbb_field = this.frm.get_docfield("items", "serial_and_batch_bundle");
if (sbb_field) {
sbb_field.get_route_options_for_new_doc = (row) => {
return {
item_code: row.doc.item_code,
voucher_type: this.frm.doc.doctype,
};
};
}
}
}
scan_barcode() {
frappe.flags.dialog_set = false;
this.barcode_scanner.process_scan();

View File

@@ -475,10 +475,10 @@
"report_hide": 1
},
{
"description": "Transactions are blocked when the outstanding balance exceeds the credit limit. When Restrict Customer Over Billing is enabled, new invoices are also blocked when the customer's overdue amount exceeds the Overdue Limit.",
"description": "Transactions are blocked or warned when outstanding balance exceeds this amount.",
"fieldname": "credit_limits",
"fieldtype": "Table",
"label": "Credit & Overdue Limits",
"label": "Credit Limit",
"options": "Customer Credit Limit",
"show_description_on_click": 1
},

View File

@@ -16,7 +16,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.utils.rename_doc import update_linked_doctypes
from frappe.query_builder import Field, functions
from frappe.utils import cint, cstr, flt, fmt_money, get_formatted_email, getdate, today
from frappe.utils import cint, cstr, flt, get_formatted_email, today
from frappe.utils.user import get_users_with_role
from erpnext.accounts.party import (
@@ -204,21 +204,17 @@ class Customer(TransactionBase):
self.credit_limits = []
self.payment_terms = self.default_price_list = ""
tables = [
["accounts", ["account"]],
["credit_limits", ["credit_limit", "overdue_billing_threshold"]],
]
tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]
fields = ["payment_terms", "default_price_list"]
for row in tables:
table, table_fields = row[0], row[1]
table, field = row[0], row[1]
if not doc.get(table):
continue
for entry in doc.get(table):
child = self.append(table)
child.update({"company": entry.company})
child.update({field: entry.get(field) for field in table_fields})
child.update({"company": entry.company, field: entry.get(field)})
for field in fields:
if not doc.get(field):
@@ -407,9 +403,6 @@ class Customer(TransactionBase):
else:
company_record.append(limit.company)
if not flt(limit.credit_limit):
continue
outstanding_amt = get_customer_outstanding(
self.name, limit.company, ignore_outstanding_sales_order=limit.bypass_credit_limit_check
)
@@ -681,124 +674,6 @@ def send_emails(customer, customer_outstanding, credit_limit, credit_controller_
frappe.sendmail(recipients=credit_controller_users_list, subject=subject, message=message)
def check_overdue_billing_threshold(customer: str, company: str) -> None:
if not frappe.get_single_value("Accounts Settings", "enable_overdue_billing_threshold"):
return
threshold = get_overdue_billing_threshold(customer, company)
if not threshold:
return
overdue_amount = get_customer_overdue_amount(customer, company)
if overdue_amount <= threshold:
return
bypass_role = frappe.get_single_value("Accounts Settings", "role_allowed_to_bypass_overdue_billing")
if bypass_role and bypass_role in frappe.get_roles():
return
company_currency = frappe.get_cached_value("Company", company, "default_currency")
frappe.throw(
_("Overdue Limit crossed for customer {0}. Overdue amount {1} exceeds the allowed limit {2}.").format(
customer,
fmt_money(overdue_amount, currency=company_currency),
fmt_money(threshold, currency=company_currency),
),
title=_("Overdue Limit Crossed"),
)
def get_overdue_billing_threshold(customer: str, company: str) -> float:
"""Overdue limit set on the customer, falling back to its customer group."""
threshold = frappe.db.get_value(
"Customer Credit Limit",
{"parent": customer, "parenttype": "Customer", "company": company},
"overdue_billing_threshold",
)
if not threshold:
customer_group = frappe.get_cached_value("Customer", customer, "customer_group")
threshold = frappe.db.get_value(
"Customer Credit Limit",
{"parent": customer_group, "parenttype": "Customer Group", "company": company},
"overdue_billing_threshold",
)
return flt(threshold)
def get_customer_overdue_amount(customer: str, company: str) -> float:
"""Amount the customer owes past its due date, in company currency.
Follows the same rule as the Overdue invoice status, so a customer is only
blocked for what the invoice list already shows as overdue.
"""
invoices = get_outstanding_invoices_for_customer(customer, company)
if not invoices:
return 0.0
payable_amounts = get_past_due_payable_amounts([d.name for d in invoices])
return flt(sum(get_overdue_portion(d, payable_amounts.get(d.name)) for d in invoices))
def get_outstanding_invoices_for_customer(customer: str, company: str) -> list[frappe._dict]:
from frappe.query_builder.functions import Sum
gl_entry = frappe.qb.DocType("GL Entry")
sales_invoice = frappe.qb.DocType("Sales Invoice")
# debit - credit is always booked in company currency, so this is comparable to the overdue limit
outstanding = Sum(gl_entry.debit) - Sum(gl_entry.credit)
return (
frappe.qb.from_(gl_entry)
.inner_join(sales_invoice)
.on(sales_invoice.name == gl_entry.against_voucher)
.select(
sales_invoice.name,
sales_invoice.due_date,
sales_invoice.base_grand_total,
outstanding.as_("outstanding"),
)
.where(gl_entry.party_type == "Customer")
.where(gl_entry.party == customer)
.where(gl_entry.company == company)
.where(gl_entry.is_cancelled == 0)
.where(gl_entry.against_voucher_type == "Sales Invoice")
.groupby(sales_invoice.name, sales_invoice.due_date, sales_invoice.base_grand_total)
.having(outstanding > 0)
).run(as_dict=True)
def get_past_due_payable_amounts(invoices: list[str]) -> dict[str, float]:
from frappe.query_builder.functions import Sum
payment_schedule = frappe.qb.DocType("Payment Schedule")
rows = (
frappe.qb.from_(payment_schedule)
.select(payment_schedule.parent, Sum(payment_schedule.base_payment_amount).as_("payable"))
.where(payment_schedule.parenttype == "Sales Invoice")
.where(payment_schedule.parent.isin(invoices))
.where(payment_schedule.due_date < getdate())
.groupby(payment_schedule.parent)
).run(as_dict=True)
return {d.parent: flt(d.payable) for d in rows}
def get_overdue_portion(invoice: frappe._dict, payable_amount: float | None) -> float:
outstanding = flt(invoice.outstanding)
# No payable amount means either a schedule-less invoice (POS, opening) or one whose terms are
# all still in the future. Both are answered by the invoice due date, which is the last term.
if payable_amount is None:
return outstanding if invoice.due_date and getdate(invoice.due_date) < getdate() else 0.0
paid = flt(invoice.base_grand_total) - outstanding
return min(max(payable_amount - paid, 0.0), outstanding)
def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None):
# Outstanding based on GL Entries
cond = ""

View File

@@ -5,15 +5,13 @@
import json
import frappe
from frappe.utils import add_days, flt, getdate, nowdate
from frappe.utils import flt, nowdate
from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import (
get_credit_limit,
get_customer_outstanding,
get_customer_overdue_amount,
get_overdue_billing_threshold,
make_quotation,
parse_full_name,
)
@@ -79,11 +77,7 @@ class TestCustomer(ERPNextTestSuite):
"company": "_Test Company",
"account": "Creditors - _TC",
}
test_credit_limits = {
"company": "_Test Company",
"credit_limit": 350000,
"overdue_billing_threshold": 5000,
}
test_credit_limits = {"company": "_Test Company", "credit_limit": 350000}
doc.append("accounts", test_account_details)
doc.append("credit_limits", test_credit_limits)
doc.insert()
@@ -103,7 +97,6 @@ class TestCustomer(ERPNextTestSuite):
self.assertEqual(c_doc.credit_limits[0].company, "_Test Company")
self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000)
self.assertEqual(c_doc.credit_limits[0].overdue_billing_threshold, 5000)
c_doc.delete()
doc.delete()
@@ -359,139 +352,6 @@ class TestCustomer(ERPNextTestSuite):
)
self.assertRaises(frappe.ValidationError, customer.save)
def test_get_customer_overdue_amount(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
baseline = get_customer_overdue_amount("_Test Customer", "_Test Company")
# a past-due, unpaid invoice adds its outstanding to the overdue amount
create_sales_invoice(qty=1, rate=500, posting_date=add_days(nowdate(), -30))
self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline + 500)
# an invoice due today (not yet past due) does not
create_sales_invoice(qty=1, rate=700, posting_date=nowdate())
self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline + 500)
def test_get_customer_overdue_amount_is_in_company_currency(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
baseline = get_customer_overdue_amount("_Test Customer USD", "_Test Company")
# 100 USD at a conversion rate of 50 must be counted as 5000 in company currency
create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
qty=1,
rate=100,
posting_date=add_days(nowdate(), -30),
)
self.assertEqual(get_customer_overdue_amount("_Test Customer USD", "_Test Company"), baseline + 5000)
def test_get_customer_overdue_amount_follows_payment_terms(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
def make_invoice_with_terms():
si = create_sales_invoice(
qty=1, rate=1200, posting_date=add_days(nowdate(), -60), do_not_save=True
)
si.append("payment_schedule", {"due_date": add_days(nowdate(), -60), "invoice_portion": 50})
si.append("payment_schedule", {"due_date": add_days(nowdate(), 30), "invoice_portion": 50})
si.insert()
si.submit()
return si
baseline = get_customer_overdue_amount("_Test Customer", "_Test Company")
# only the term that has fallen due counts, not the whole 1200 balance. The invoice due_date
# is the last term (in 30 days), so this is only caught by reading the payment schedule.
si = make_invoice_with_terms()
self.assertEqual(getdate(si.due_date), getdate(add_days(nowdate(), 30)))
self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline + 600)
# paying off the past-due term clears the overdue amount
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "_Test Overdue Payment"
pe.reference_date = nowdate()
pe.paid_amount = pe.received_amount = 600
pe.references[0].allocated_amount = 600
pe.insert()
pe.submit()
self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline)
def test_overdue_billing_threshold_on_submit(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
create_sales_invoice(qty=1, rate=1000, posting_date=add_days(nowdate(), -30))
overdue = get_customer_overdue_amount("_Test Customer", "_Test Company")
settings = frappe.get_single("Accounts Settings")
original_enable = settings.enable_overdue_billing_threshold
original_bypass_role = settings.role_allowed_to_bypass_overdue_billing
try:
settings.enable_overdue_billing_threshold = 1
settings.role_allowed_to_bypass_overdue_billing = None
settings.save()
set_overdue_billing_threshold("_Test Customer", "_Test Company", overdue - 100)
# overdue is over the threshold and the user has no bypass role -> blocked
si = create_sales_invoice(do_not_submit=True)
self.assertRaises(frappe.ValidationError, si.submit)
# a user holding the bypass role can still submit
settings.role_allowed_to_bypass_overdue_billing = "Accounts Manager"
settings.save()
si = create_sales_invoice(do_not_submit=True)
si.submit()
self.assertEqual(si.docstatus, 1)
# threshold still crossed, but the feature is off -> never blocked
settings.enable_overdue_billing_threshold = 0
settings.role_allowed_to_bypass_overdue_billing = None
settings.save()
si = create_sales_invoice(do_not_submit=True)
si.submit()
self.assertEqual(si.docstatus, 1)
finally:
settings.enable_overdue_billing_threshold = original_enable
settings.role_allowed_to_bypass_overdue_billing = original_bypass_role
settings.save()
def test_overdue_billing_threshold_falls_back_to_customer_group(self):
customer_group = frappe.get_cached_value("Customer", "_Test Customer", "customer_group")
group = frappe.get_doc("Customer Group", customer_group)
group.credit_limits = []
group.append("credit_limits", {"company": "_Test Company", "overdue_billing_threshold": 5000})
group.save()
# the customer has no threshold of its own, so the group's applies
self.assertEqual(get_overdue_billing_threshold("_Test Customer", "_Test Company"), 5000)
# a threshold on the customer wins over the group
set_overdue_billing_threshold("_Test Customer", "_Test Company", 2000)
self.assertEqual(get_overdue_billing_threshold("_Test Customer", "_Test Company"), 2000)
# a 0 on the customer inherits the group's limit
set_overdue_billing_threshold("_Test Customer", "_Test Company", 0)
self.assertEqual(get_overdue_billing_threshold("_Test Customer", "_Test Company"), 5000)
def test_overdue_threshold_row_without_credit_limit(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
# outstanding must be > 0 so a 0 credit_limit would previously trip the check
create_sales_invoice(qty=1, rate=500)
customer = frappe.get_doc("Customer", "_Test Customer")
customer.credit_limits = []
customer.append("credit_limits", {"company": "_Test Company", "overdue_billing_threshold": 1000})
customer.save()
self.assertEqual(customer.credit_limits[0].overdue_billing_threshold, 1000)
self.assertEqual(flt(customer.credit_limits[0].credit_limit), 0.0)
def test_customer_payment_terms(self):
frappe.db.set_value(
"Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3"
@@ -591,18 +451,6 @@ def set_credit_limit(customer, company, credit_limit):
customer.credit_limits[-1].db_insert()
def set_overdue_billing_threshold(customer, company, threshold):
customer = frappe.get_doc("Customer", customer)
for d in customer.credit_limits:
if d.company == company:
d.overdue_billing_threshold = threshold
d.db_update()
return
customer.append("credit_limits", {"company": company, "overdue_billing_threshold": threshold})
customer.credit_limits[-1].db_insert()
def create_internal_customer(customer_name=None, represents_company=None, allowed_to_interact_with=None):
if not customer_name:
customer_name = represents_company

View File

@@ -8,7 +8,6 @@
"company",
"column_break_2",
"credit_limit",
"overdue_billing_threshold",
"bypass_credit_limit_check"
],
"fields": [
@@ -19,15 +18,6 @@
"in_list_view": 1,
"label": "Credit Limit"
},
{
"columns": 3,
"description": "New Sales Invoices are blocked when the customer's overdue amount exceeds this. Requires 'Restrict Customer Over Billing' in Accounts Settings.",
"fieldname": "overdue_billing_threshold",
"fieldtype": "Currency",
"hidden": 1,
"in_list_view": 1,
"label": "Overdue Limit"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"

View File

@@ -18,7 +18,6 @@ class CustomerCreditLimit(Document):
bypass_credit_limit_check: DF.Check
company: DF.Link | None
credit_limit: DF.Currency
overdue_billing_threshold: DF.Currency
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -132,7 +132,7 @@
{
"fieldname": "credit_limits",
"fieldtype": "Table",
"label": "Credit & Overdue Limits",
"label": "Credit Limit",
"options": "Customer Credit Limit"
}
],

View File

@@ -296,7 +296,7 @@ def get_batches_by_oldest(item_code, warehouse):
"""Returns the oldest batch and qty for the given item_code and warehouse"""
batches = get_batch_qty(item_code=item_code, warehouse=warehouse)
batches_dates = [[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches]
batches_dates.sort(key=lambda tup: (tup[1] is None, tup[1]))
batches_dates.sort(key=lambda tup: tup[1])
return batches_dates

View File

@@ -1381,9 +1381,6 @@ def create_delivery_wo_so(pick_list, target, target_doc=None):
target_doc.company = pick_list.company
if not target_doc.customer:
target_doc.customer = pick_list.customer
item_table_mapper_without_so = {
"doctype": f"{target} Item",
"field_map": {

View File

@@ -242,7 +242,7 @@ class RepostItemValuation(Document):
def clear_attachment(self):
if attachments := get_attachments(self.doctype, self.name):
attachment = attachments[0]
frappe.delete_doc("File", attachment.name, ignore_permissions=True, force=True)
frappe.delete_doc("File", attachment.name, ignore_permissions=True)
if self.reposting_data_file:
self.db_set("reposting_data_file", None)

View File

@@ -515,34 +515,6 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin):
)
)
def test_clear_attachment_skips_referenced_data_file(self):
riv = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Item and Warehouse",
"company": "_Test Company",
"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC",
"posting_date": today(),
}
).insert(ignore_permissions=True)
attached = frappe.get_doc(
{
"doctype": "File",
"file_name": "repost_data.json.gz",
"content": "test",
"attached_to_doctype": riv.doctype,
"attached_to_name": riv.name,
}
).insert(ignore_permissions=True)
riv.db_set("reposting_data_file", attached.file_url)
riv.clear_attachment()
self.assertFalse(frappe.db.exists("File", attached.name))
self.assertIsNone(frappe.db.get_value("Repost Item Valuation", riv.name, "reposting_data_file"))
@ERPNextTestSuite.change_settings(
"Stock Reposting Settings",
{"item_based_reposting": 1, "enable_parallel_reposting": 1, "no_of_parallel_reposting": 2},

View File

@@ -569,6 +569,8 @@ frappe.ui.form.on("Stock Entry", {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
frm.events.set_route_options_for_new_doc(frm);
frm.set_df_property(
"items",
"cannot_add_rows",
@@ -581,6 +583,28 @@ frappe.ui.form.on("Stock Entry", {
);
},
set_route_options_for_new_doc(frm) {
let batch_no_field = frm.get_docfield("items", "batch_no");
if (batch_no_field) {
batch_no_field.get_route_options_for_new_doc = function (row) {
return {
item: row.doc.item_code,
};
};
}
let sbb_field = frm.get_docfield("items", "serial_and_batch_bundle");
if (sbb_field) {
sbb_field.get_route_options_for_new_doc = (row) => {
return {
item_code: row.doc.item_code,
voucher_type: frm.doc.doctype,
warehouse: row.doc.s_warehouse || row.doc.t_warehouse,
};
};
}
},
get_items_from_transit_entry: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.subcontracting_inward_order) {
frm.add_custom_button(
@@ -1288,7 +1312,6 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
}
onload_post_render() {
super.onload_post_render();
var me = this;
if (me.frm.doc.__islocal && me.frm.doc.company && !me.frm.doc.amended_from) {
me.company();

View File

@@ -1344,47 +1344,6 @@ class TestStockLedgerEntry(ERPNextTestSuite, StockTestMixin):
# receipt2 now sits on a zero base -> 10 (not 0 from a double shift, nor a negative-stock error).
self.assertEqual(qty_after(receipt2), 10)
def test_cancel_shifts_same_timestamp_delivery_notes(self):
item = make_item().name
warehouse = "_Test Warehouse - _TC"
posting_date = today()
posting_time = "10:00:00"
make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=100,
rate=10,
posting_date=posting_date,
posting_time="09:00:00",
)
dns = []
for i in range(5):
dns.append(
create_delivery_note(
item_code=item,
warehouse=warehouse,
qty=20,
rate=10 * i,
posting_date=posting_date,
posting_time=posting_time,
)
)
time.sleep(1)
dn = dns[2]
dn.cancel()
expected_qty_after_transaction_of_dns3 = 40
qty_after_transaction_of_dns3 = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": dns[3].name, "is_cancelled": 0},
"qty_after_transaction",
)
self.assertEqual(expected_qty_after_transaction_of_dns3, qty_after_transaction_of_dns3)
def test_get_next_stock_reco_respects_creation_order(self):
# A stock reco sharing the exact posting timestamp of the current entry must only count as the
# "next" reco when it was created after that entry. A reco created before it actually precedes

View File

@@ -46,6 +46,17 @@ frappe.ui.form.on("Stock Reconciliation", {
};
});
let sbb_field = frm.get_docfield("items", "serial_and_batch_bundle");
if (sbb_field) {
sbb_field.get_route_options_for_new_doc = (row) => {
return {
item_code: row.doc.item_code,
warehouse: row.doc.warehouse,
voucher_type: frm.doc.doctype,
};
};
}
if (frm.doc.company) {
erpnext.queries.setup_queries(frm, "Warehouse", function () {
return erpnext.queries.warehouse(frm.doc);

View File

@@ -325,7 +325,6 @@ class FIFOSlots:
del stock_ledger_entries
self._recompute_moving_average_slots()
self._rebalance_negative_batch_slots()
if not self.filters.get("show_warehouse_wise_stock"):
# (Item 1, WH 1), (Item 1, WH 2) => (Item 1)
@@ -347,33 +346,6 @@ class FIFOSlots:
if is_qty_slot(slot):
slot[FIFO_VALUE_INDEX] = flt(slot[FIFO_QTY_INDEX] * rate)
def _rebalance_negative_batch_slots(self) -> None:
for item_dict in self.item_details.values():
if item_dict.get("has_batch_no"):
self._rebalance_negative_batch_slot_values(item_dict["fifo_queue"])
def _rebalance_negative_batch_slot_values(self, fifo_queue: list) -> None:
"""A batch is one valuation pool, so a slot driven negative by consumption
at the pooled rate is stale detail: spread the pool value over its slots."""
groups = {}
for slot in fifo_queue:
if is_batch_slot(slot):
key = slot[BATCH_SLOT_BATCH_INDEX] if slot[BATCH_SLOT_VALUATION_INDEX] else None
groups.setdefault(key, []).append(slot)
for slots in groups.values():
has_negative_slot = any(
flt(slot[BATCH_SLOT_VALUE_INDEX]) < 0 and flt(slot[BATCH_SLOT_QTY_INDEX]) > 0
for slot in slots
)
total_qty = sum(flt(slot[BATCH_SLOT_QTY_INDEX]) for slot in slots)
if not has_negative_slot or total_qty <= 0:
continue
rate = sum(flt(slot[BATCH_SLOT_VALUE_INDEX]) for slot in slots) / total_qty
for slot in slots:
slot[BATCH_SLOT_VALUE_INDEX] = flt(slot[BATCH_SLOT_QTY_INDEX] * rate)
def _get_bundle_wise_details(self, stock_ledger_entries: list | None) -> tuple[dict, dict]:
if stock_ledger_entries is not None:
return frappe._dict({}), frappe._dict({})

View File

@@ -616,58 +616,6 @@ class TestStockAgeing(ERPNextTestSuite):
],
)
def test_batch_issue_at_pooled_rate_keeps_slot_values_positive(self):
"""Ledger (same wh, batch B): [+10 @ 0, +10 @ 10, -4 @ pooled 5]
Consuming the zero-valued head slot at the pooled rate drives it
negative; slot values are then rebalanced to the batch pool rate."""
from erpnext.stock.doctype.item.test_item import make_item
item_code = make_item(
"Test Stock Ageing Batch Pool Rebalance",
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
).name
batch_no = "SA-POOL-REBALANCE-BATCH"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
ignore_permissions=True
)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
return frappe._dict(
name=item_code,
actual_qty=actual_qty,
qty_after_transaction=qty_after,
stock_value_difference=stock_value_difference,
valuation_rate=abs(stock_value_difference / actual_qty) if actual_qty else 0,
warehouse="WH 1",
posting_date=posting_date,
voucher_type="Stock Entry",
voucher_no=voucher_no,
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=batch_no,
)
sle = [
make_sle("2021-12-01", "001", 10, 10, 0),
make_sle("2021-12-02", "002", 10, 20, 100),
make_sle("2021-12-03", "003", -4, 16, -20),
]
slots = FIFOSlots(self.filters, sle).generate()
queue = slots[item_code]["fifo_queue"]
self.assertEqual(
queue,
[
[batch_no, 1, 6.0, "2021-12-01", 30.0],
[batch_no, 1, 10.0, "2021-12-01", 50.0],
],
)
def test_sequential_stock_reco_same_warehouse(self):
"""
Test back to back stock recos (same warehouse).

View File

@@ -136,7 +136,7 @@ frappe.query_reports["Stock Balance"] = {
fieldname: "include_zero_stock_items",
label: __("Include Zero Stock Items"),
fieldtype: "Check",
default: 1,
default: 0,
},
{
fieldname: "show_dimension_wise_stock",

View File

@@ -497,9 +497,7 @@ class update_entries_after:
self.data = frappe._dict()
if (not self.repost_doc or not self.args.get("item_wh_wise_last_posted_sle")) and not self.args.get(
"cancelled"
):
if not self.repost_doc or not self.args.get("item_wh_wise_last_posted_sle"):
self.initialize_previous_data(self.args)
self.build()
@@ -807,23 +805,10 @@ class update_entries_after:
def process_sle_against_current_timestamp(self):
sl_entries = get_sle_against_current_voucher(self.args)
if self.args.get("cancelled") and sl_entries:
self.seed_previous_sle_for_cancellation(sl_entries[0])
for sle in sl_entries:
sle["timestamp"] = sle.posting_datetime
self.process_sle(sle)
def seed_previous_sle_for_cancellation(self, anchor_sle):
key = (anchor_sle.item_code, anchor_sle.warehouse)
if key in self.prev_sle_dict:
return
args = frappe._dict(anchor_sle)
args["sle_id"] = args.name
prev_sle = get_previous_sle_of_current_voucher(args)
if prev_sle:
self.prev_sle_dict[key] = prev_sle
def get_future_entries_to_fix(self):
# includes current entry!
args = self.data[self.args.warehouse].previous_sle or frappe._dict(