Compare commits

..

124 Commits

Author SHA1 Message Date
Frappe PR Bot
9a755ca23d chore(release): Bumped to Version 14.23.4
## [14.23.4](https://github.com/frappe/erpnext/compare/v14.23.3...v14.23.4) (2023-05-06)

### Bug Fixes

* handle empty FBs properly in TB and GL [v14] (backport [#35189](https://github.com/frappe/erpnext/issues/35189)) ([#35191](https://github.com/frappe/erpnext/issues/35191)) ([a5a08c9](a5a08c9889))
2023-05-06 11:12:52 +00:00
mergify[bot]
a5a08c9889 fix: handle empty FBs properly in TB and GL [v14] (backport #35189) (#35191)
fix: handle empty FBs properly in TB and GL [v14] (#35189)

fix: handle empty FBs properly in TB and GL
(cherry picked from commit ed5f39c2c2)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-05-06 16:41:10 +05:30
Frappe PR Bot
a1842103b6 chore(release): Bumped to Version 14.23.3
## [14.23.3](https://github.com/frappe/erpnext/compare/v14.23.2...v14.23.3) (2023-05-04)

### Bug Fixes

* internal transfer condition ([736c34e](736c34e61a))
* not allow to transfer excess materials against the job card ([580641f](580641f55c))
2023-05-04 18:02:30 +00:00
rohitwaghchaure
4d54430010 Merge pull request #35174 from frappe/mergify/bp/version-14/pr-35163
fix: internal transfer condition (backport #35158) (backport #35163)
2023-05-04 23:30:29 +05:30
rohitwaghchaure
e855866820 Merge pull request #35175 from frappe/mergify/bp/version-14/pr-35169
fix: not allow to transfer excess materials against the job card (backport #35167) (backport #35169)
2023-05-04 23:30:07 +05:30
Rohit Waghchaure
580641f55c fix: not allow to transfer excess materials against the job card
(cherry picked from commit 8167b24219)
(cherry picked from commit b0c042de1b)
2023-05-04 17:11:54 +00:00
Rohit Waghchaure
736c34e61a fix: internal transfer condition
(cherry picked from commit b5a2ccf21d)
(cherry picked from commit f5f4902494)
2023-05-04 17:10:41 +00:00
Frappe PR Bot
144594b3e1 chore(release): Bumped to Version 14.23.2
## [14.23.2](https://github.com/frappe/erpnext/compare/v14.23.1...v14.23.2) (2023-05-04)

### Bug Fixes

* over production percentage not considered in validation ([4e83d0b](4e83d0baa6))
2023-05-04 04:54:00 +00:00
rohitwaghchaure
64b416b4dd Merge pull request #35152 from frappe/mergify/bp/version-14/pr-35149
fix: over production percentage not considered in validation (backport #35148) (backport #35149)
2023-05-04 10:22:33 +05:30
Rohit Waghchaure
4e83d0baa6 fix: over production percentage not considered in validation
(cherry picked from commit a84d0af81e)
(cherry picked from commit bf6e1b67a5)
2023-05-04 04:29:28 +00:00
Frappe PR Bot
5eb5bf7102 chore(release): Bumped to Version 14.23.1
## [14.23.1](https://github.com/frappe/erpnext/compare/v14.23.0...v14.23.1) (2023-05-03)

### Bug Fixes

* conflicts ([3ba2b9e](3ba2b9ed2e))
* don't allow to make reposting for the closed period ([cca2fce](cca2fcec54))
* don't create material request from sales order against the delivered items ([a5489ee](a5489ee2ac))
* handle expected_value_after_useful_life properly in asset value adjustment ([#35117](https://github.com/frappe/erpnext/issues/35117)) ([80230fe](80230fec3e))
* handle finance book properly in trial balance and general ledger [v14] ([#35136](https://github.com/frappe/erpnext/issues/35136)) ([344c339](344c339484))
* Hyperlink in Quality Inspection Summary ([e4ce6fa](e4ce6fa195))
* incorrect paid_amount and exchange rate in PE ([f50b4d8](f50b4d80f1))
* linter issue ([78c34d7](78c34d71e2))
* linter issue ([453249d](453249d868))
* Naming series error in Journal Entry template ([#35084](https://github.com/frappe/erpnext/issues/35084)) ([7021e3a](7021e3adb1))
* not able to create delivery note from sales order ([28dfc13](28dfc13dc6))
* Report link, option, and added a link for Sales Person in GP ([fc611cf](fc611cf86b))
* test case ([35ec125](35ec125b34))
* test case ([6597c74](6597c74d6c))
* test case ([d844a2b](d844a2b990))
* timeout error while submitting delivery note ([e33fb3b](e33fb3b242))
* Updates in process statement of Accounts ([#35064](https://github.com/frappe/erpnext/issues/35064)) ([bef9dd7](bef9dd79e7))
2023-05-03 04:22:30 +00:00
Deepesh Garg
51d9d0a454 Merge pull request #35131 from frappe/version-14-hotfix
chore: release v14
2023-05-03 09:50:51 +05:30
Anand Baburajan
344c339484 fix: handle finance book properly in trial balance and general ledger [v14] (#35136)
fix: handle FBs properly in general ledger and trial balance
2023-05-03 07:11:01 +05:30
mergify[bot]
bef9dd79e7 fix: Updates in process statement of Accounts (#35064)
* fix: Updates in process statement of Accounts (#35064)

(cherry picked from commit ea0b03ae9e)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json

* chore: resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-05-02 23:06:52 +05:30
rohitwaghchaure
815696964a Merge pull request #35121 from frappe/mergify/bp/version-14-hotfix/pr-35118
fix: don't allow to make reposting for the closed period (backport #35118)
2023-05-02 18:02:23 +05:30
ruthra kumar
f0ae02e921 Merge pull request #35132 from frappe/mergify/bp/version-14-hotfix/pr-35107
fix: incorrect paid_amount and exchange rate in Payment Entry (backport #35107)
2023-05-02 15:43:08 +05:30
ruthra kumar
f50b4d80f1 fix: incorrect paid_amount and exchange rate in PE
If Company master has no default cash or bank account set but Party has
default company bank account set. In this case, paid_amount and
conversion rate are not calculated correctly

(cherry picked from commit 123355392b)
2023-05-02 09:39:03 +00:00
ruthra kumar
7ad795bec5 Merge pull request #35129 from frappe/mergify/bp/version-14-hotfix/pr-35091
refactor: button to toggle parent doc cost center preference for rounding adjustment amount (backport #35091)
2023-05-02 15:07:54 +05:30
ruthra kumar
662d9cb5aa Merge pull request #35128 from frappe/mergify/bp/version-14-hotfix/pr-35061
refactor: don't book exchange gain/loss for sales/purchase orders (backport #35061)
2023-05-02 15:07:09 +05:30
ruthra kumar
8ac718d98c refactor: checkbox to toggle parent doc cost center preference
(cherry picked from commit 4ccce93394)
2023-05-02 08:38:13 +00:00
ruthra kumar
3810b02023 refactor: checkbox in Sales Invoice
(cherry picked from commit 0f3b06cc8a)
2023-05-02 08:38:13 +00:00
ruthra kumar
25b37737a2 refactor: checkbox to toggle parent doc cost center preference
(cherry picked from commit ebe6787510)
2023-05-02 08:38:13 +00:00
ruthra kumar
0e39e5e868 refactor: checkbox in purchase invoice
(cherry picked from commit b44331c981)
2023-05-02 08:38:13 +00:00
ruthra kumar
fedde7fe3b test: Sales/Purchase Orders will not book Exchange gain/loss
(cherry picked from commit ce4e18c8d2)
2023-05-02 08:34:53 +00:00
ruthra kumar
551c96e1e5 refactor: don't book exch gain/loss for sales/purchase orders
(cherry picked from commit effb34bbfa)
2023-05-02 08:34:53 +00:00
rohitwaghchaure
78c34d71e2 fix: linter issue 2023-05-02 14:01:26 +05:30
rohitwaghchaure
453249d868 fix: linter issue 2023-05-02 11:08:06 +05:30
rohitwaghchaure
35ec125b34 fix: test case 2023-05-02 11:00:12 +05:30
rohitwaghchaure
2f3ed23a9d Merge pull request #35125 from frappe/mergify/bp/version-14-hotfix/pr-35123
fix: timeout error while submitting delivery note (backport #35123)
2023-05-02 00:00:46 +05:30
rohitwaghchaure
6597c74d6c fix: test case 2023-05-01 23:32:54 +05:30
Rohit Waghchaure
e33fb3b242 fix: timeout error while submitting delivery note
(cherry picked from commit 2d5ccc07b1)
2023-05-01 18:01:02 +00:00
rohitwaghchaure
d844a2b990 fix: test case 2023-05-01 20:49:17 +05:30
rohitwaghchaure
3ba2b9ed2e fix: conflicts 2023-05-01 20:47:12 +05:30
Rohit Waghchaure
cca2fcec54 fix: don't allow to make reposting for the closed period
(cherry picked from commit f751727149)

# Conflicts:
#	erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
2023-05-01 15:12:01 +00:00
Anand Baburajan
80230fec3e fix: handle expected_value_after_useful_life properly in asset value adjustment (#35117) 2023-05-01 19:53:39 +05:30
mergify[bot]
7021e3adb1 fix: Naming series error in Journal Entry template (#35084)
fix: Naming series error in Journal Entry template (#35084)

(cherry picked from commit f3b3dabb9a)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-05-01 13:02:11 +05:30
rohitwaghchaure
69c0d81c55 Merge pull request #35093 from frappe/mergify/bp/version-14-hotfix/pr-35092
fix: not able to create delivery note from sales order (backport #35092)
2023-04-30 14:54:43 +05:30
Rohit Waghchaure
28dfc13dc6 fix: not able to create delivery note from sales order
(cherry picked from commit bdf2f7416a)
2023-04-28 10:33:23 +00:00
rohitwaghchaure
5f381cd520 Merge pull request #35070 from frappe/mergify/bp/version-14-hotfix/pr-35066
fix: don't create material request from sales order against delivered items (backport #35066)
2023-04-28 16:02:15 +05:30
Sagar Sharma
8571e6c5d2 Merge pull request #35088 from frappe/mergify/bp/version-14-hotfix/pr-35081
fix: Hyperlink in Quality Inspection Summary (backport #35081)
2023-04-28 12:53:45 +05:30
Nihantra Patel
e4ce6fa195 fix: Hyperlink in Quality Inspection Summary
(cherry picked from commit 72dd7884a8)
2023-04-28 07:19:15 +00:00
ruthra kumar
ef3d352a17 Merge pull request #35080 from frappe/mergify/bp/version-14-hotfix/pr-35077
fix: Report link, option, and added a link for Sales Person in GP (backport #35077)
2023-04-27 17:42:30 +05:30
Nihantra Patel
fc611cf86b fix: Report link, option, and added a link for Sales Person in GP
(cherry picked from commit 6dfca79af3)
2023-04-27 11:12:01 +00:00
Rohit Waghchaure
a5489ee2ac fix: don't create material request from sales order against the delivered items
(cherry picked from commit 1e2deee579)
2023-04-27 03:36:23 +00:00
Frappe PR Bot
85f3a5d318 chore(release): Bumped to Version 14.23.0
# [14.23.0](https://github.com/frappe/erpnext/compare/v14.22.3...v14.23.0) (2023-04-25)

### Bug Fixes

* `PermissionError` in Work Order ([a3568c1](a3568c1b27))
* Add company field to lower deduction certificate ([#34914](https://github.com/frappe/erpnext/issues/34914)) ([d732083](d732083166))
* add item-code filter for SCR supplied-items batch-no ([7c4f83e](7c4f83ed60))
* add limit for get_next_stock_reco ([#34937](https://github.com/frappe/erpnext/issues/34937)) ([7131ff2](7131ff28fd))
* added validation for extra job card ([a5fde5d](a5fde5d933))
* batch qty conversion factor issue fixed in pos transaction ([#34917](https://github.com/frappe/erpnext/issues/34917)) ([56ef0ba](56ef0baa9d))
* broken 'set exchange gain/loss' btn in payment entry ([#34940](https://github.com/frappe/erpnext/issues/34940)) ([00968ba](00968badf5))
* Bulk Payment Entry from PO/SO ([#34942](https://github.com/frappe/erpnext/issues/34942)) ([878d747](878d7477bc))
* click handler should not attempt indexed access of empty array ([#35013](https://github.com/frappe/erpnext/issues/35013)) ([06f204a](06f204a8d6))
* Common party JV cost center ([#35008](https://github.com/frappe/erpnext/issues/35008)) ([693007a](693007adfe))
* duplicate reposting entries of same voucher ([aeac43c](aeac43ccf9))
* FEC report for France accountancy  ([#34781](https://github.com/frappe/erpnext/issues/34781)) ([6aabab2](6aabab26d8))
* filtering via batch no ([#34951](https://github.com/frappe/erpnext/issues/34951)) ([3b23fc1](3b23fc1eba)), closes [#34950](https://github.com/frappe/erpnext/issues/34950)
* Incorrect difference value in Stock and Account Value Comparison report ([e7ca833](e7ca833929))
* incorrect OR condition causing timeout error ([c020789](c020789bfc))
* internal Purchase Receipt GL Entries ([f7e436f](f7e436fe71))
* item not showing in the BOM ([28cd79a](28cd79a040))
* pass reference_doctype in link queries (backport [#35038](https://github.com/frappe/erpnext/issues/35038)) ([#35039](https://github.com/frappe/erpnext/issues/35039)) ([5cc3978](5cc3978c16))
* Payment entry with TDS in bank reco statement ([#34961](https://github.com/frappe/erpnext/issues/34961)) ([f43ea0d](f43ea0d6ff))
* per_billed condition for Payment Entry ([#34969](https://github.com/frappe/erpnext/issues/34969)) ([f9f42c7](f9f42c7e98))
* process_loss_percentage in BOM ([4418862](4418862965))
* removed depends on for the Employee Detail section ([9d17d3f](9d17d3ff06))
* respect title_field from doctype to bulk transactions ([#34928](https://github.com/frappe/erpnext/issues/34928)) ([29aa4a0](29aa4a0222))
* set `frappe.flags.company` to call regional code accurately ([789dfd6](789dfd6774))
* simplify `erpnext.get_region` ([87595bd](87595bdb7e))
* SLA permissions (backport [#34981](https://github.com/frappe/erpnext/issues/34981)) ([#34986](https://github.com/frappe/erpnext/issues/34986)) ([83a1b83](83a1b836f9))
* stock entry type issue ([33a1608](33a16086ef))
* **test:** `test_backdated_stock_reco_cancellation_future_negative_stock` ([2ad157b](2ad157bd77))
* **test:** `test_internal_pr_gl_entries` ([7740ceb](7740ceb27e))
* Unable to allocate advance against invoice ([#35007](https://github.com/frappe/erpnext/issues/35007)) ([5045ad6](5045ad6be6))
* use `functools.wraps` to preserve doc signature ([2ec18eb](2ec18eb4cf))
* use CombineDatetime instead of Timestamp in QB queries ([55da91c](55da91cb34))
* use filter_by_finance_book instead of only_depreciable_assets in fixed asset register (backport [#35031](https://github.com/frappe/erpnext/issues/35031)) ([#35035](https://github.com/frappe/erpnext/issues/35035)) ([5630e81](5630e8189b))
* Use set instead of db_set as it is called from validate ([#34967](https://github.com/frappe/erpnext/issues/34967)) ([6650212](665021270f))
* v14, Bank Reconcile Tools not cover case JV debit bank ([#35000](https://github.com/frappe/erpnext/issues/35000)) ([82d8379](82d8379188))
* validation for internal transfer entry ([a582354](a5823547d3))
* value of depreciable assets not updating after manual depr entry [v14] ([#35010](https://github.com/frappe/erpnext/issues/35010)) ([3c75e55](3c75e55cb9))
* wrong qty of remaining work orders to be created when using "Create" > "Work Order" ([#34726](https://github.com/frappe/erpnext/issues/34726)) ([47df41f](47df41fdbd))

### Features

* Reconcile Payments in background ([#34596](https://github.com/frappe/erpnext/issues/34596)) ([5923a80](5923a80a0f))

### Performance Improvements

* Journal Entries (backport [#34918](https://github.com/frappe/erpnext/issues/34918)) ([#35054](https://github.com/frappe/erpnext/issues/35054)) ([39b5147](39b5147768))

### Reverts

* Revert "fix: Rate from LDC in TDS reports (#33699)" ([f44a79f](f44a79fa73)), closes [#33699](https://github.com/frappe/erpnext/issues/33699)
2023-04-25 17:00:40 +00:00
Deepesh Garg
b1473c9932 Merge pull request #35034 from frappe/version-14-hotfix
chore: release v14
2023-04-25 22:28:48 +05:30
mergify[bot]
82d8379188 fix: v14, Bank Reconcile Tools not cover case JV debit bank (#35000)
fix: v14, Bank Reconcile Tools not cover case JV debit bank (#35000)

(cherry picked from commit c36dc3dc57)

Co-authored-by: Kitti U. @ Ecosoft <kittiu@gmail.com>
2023-04-25 21:58:27 +05:30
mergify[bot]
39b5147768 perf: Journal Entries (backport #34918) (#35054)
* refactor: rewrite `get_stock_value_on()` queries in `QB`

(cherry picked from commit e43bc38e05)

* refactor: sum up SLE value in query

(cherry picked from commit 9a37ac6c25)

* refactor: `get_stock_value_on()` to get stock value of multiple warehouses at once

(cherry picked from commit e782a054c8)

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-04-25 21:53:07 +05:30
mergify[bot]
d732083166 fix: Add company field to lower deduction certificate (#34914)
* fix: Add company field to lower deduction certificate (#34914)

(cherry picked from commit b545e3def0)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-04-25 21:33:43 +05:30
mergify[bot]
693007adfe fix: Common party JV cost center (#35008)
fix: Common party JV cost center (#35008)

(cherry picked from commit f88431a79a)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-04-25 21:16:59 +05:30
mergify[bot]
f43ea0d6ff fix: Payment entry with TDS in bank reco statement (#34961)
fix: Payment entry with TDS in bank reco statement (#34961)

(cherry picked from commit ecea9b44a3)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-04-25 21:13:20 +05:30
mergify[bot]
f9f42c7e98 fix: per_billed condition for Payment Entry (#34969)
fix: per_billed condition for Payment Entry (#34969)

(cherry picked from commit d6bc8bba8b)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-04-25 21:12:58 +05:30
mergify[bot]
06f204a8d6 fix: click handler should not attempt indexed access of empty array (#35013)
fix: click handler should not attempt indexed access of empty array (#35013)

fix: click handler should not attempt indexed access
of empty array

(cherry picked from commit 3d90b970d1)

Co-authored-by: tundebabzy <tundebabzy@gmail.com>
2023-04-25 21:11:08 +05:30
mergify[bot]
47df41fdbd fix: wrong qty of remaining work orders to be created when using "Create" > "Work Order" (#34726)
fix: wrong qty of remaining work orders to be created when using "Create" > "Work Order" (#34726)

* fix: convert asynchronous field update to synchronous

* fix: wrong qty of remaining work orders to be created when using "Create" > "Work Order"

(cherry picked from commit 189b020d22)

Co-authored-by: danjeremynavarro <46537526+danjeremynavarro@users.noreply.github.com>
2023-04-25 20:08:01 +05:30
mergify[bot]
878d7477bc fix: Bulk Payment Entry from PO/SO (#34942)
fix: Bulk Payment Entry from PO/SO (#34942)

Co-authored-by: Nihantra Patel <n.patel.serpentcs@gmail.com>
(cherry picked from commit f1acc5fabb)

Co-authored-by: Solufy Solution <34390782+Solufyin@users.noreply.github.com>
2023-04-25 20:05:34 +05:30
mergify[bot]
29aa4a0222 fix: respect title_field from doctype to bulk transactions (#34928)
fix: respect title_field from doctype to bulk transactions (#34928)

(cherry picked from commit 22290c2694)

Co-authored-by: HarryPaulo <paulo_fabris@hotmail.com>
2023-04-25 20:05:13 +05:30
mergify[bot]
5045ad6be6 fix: Unable to allocate advance against invoice (#35007)
fix: Unable to allocate advance against invoice (#35007)

(cherry picked from commit f7b50f2ade)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-04-25 20:04:47 +05:30
mergify[bot]
665021270f fix: Use set instead of db_set as it is called from validate (#34967)
fix: Use set instead of db_set as it is called from validate (#34967)

(cherry picked from commit 72b5c1f70a)

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
2023-04-25 20:04:28 +05:30
mergify[bot]
5923a80a0f feat: Reconcile Payments in background (#34596)
feat: Reconcile Payments in background (#34596)

* feat: auto reconcile in background

* chore: Option to enable auto reconciliation in settings

* refactor: validate if feature is enabled in settings

* refactor: check for running job while using reconciliation tool

* chore: using doc to get filter values

* chore: use frappe.db.get_value in validations

* chore: cleanup commented out code

* chore: replace get_list with get_all

* chore: use block scope variable

* chore: type information for functions

* refactor: flag to ignore job validation check

* refactor: update parent doc status if all reconciled

* chore: create test_records file

* test: create a bunch of vouchers for testing auto reconcile

* chore: renamed auto_reconcile to process_payment_reconciliation

* chore: another child doctype to hold payments

* chore: remove duplicate field

* chore: add fetched payments to log

* chore: Popup comment message update

* chore: replace get_all with get_value

* chore: replace label in settings page

* chore: remove unit test and records

* refactor: status in reconciliation log

* refactor: set status in log as well

* chore: fix field name

* chore: change triggered job name

* chore: use status field in list view of log

* chore: status while there are no allocations

* refactor: split trigger function into two

* chore: adding cancelled status

* refactor: function trigger queued docs

* chore: cron job scheduled

* chore: fixing accouts settings json file

* chore: typos and variable scope

* chore: use 'pluck' in db call

* chore: remove redundant whitelist decorator

* chore: use single DB call to fetch values

* chore: replace get_all with get_value

* refactor: use raw db calls to fetch reconciliation log records

Using get_doc on `Process Payment Reconciliation Log` is costly when
handling large volumes of invoices.

Use raw frappe.db.get_all to selectively pull status and reconciled count

* chore: update status on successful batch operation

* chore: make payment table readonly

* chore: ability to pause the background job

* chore: remove isolate_each_allocation

* chore: more description in progress bar

* refactor: partially working state

* refactor: update reconcile flag and setting hard limits for fetching

* chore: make allocation editable -- NEED TO REVERT

* chore: pause button

* refactor: skip setter function in Payment Entry for better performan

* refactor: split reconcile function and skip a setter function

1. Split reconcile function into 2
2. While reconciling against payment entry, skip a
set_missing_ref_details setter method

* chore: increase payment limit

* refactor: replace frappe.db.get_all with frappe.db.get_value

* chore: remove unwanted doctypes

* refactor: make allocation table readonly

* perf: update ref_details only for newly linked invoices

* chore: rename skip flag

* refactor(UI): receivable_payable field should auto populate

* refactor: no control statements in finally block

* chore: cleanup section and rename checkbox

* chore: update new fieldname in code

* chore: update error msg

* refactor: start and pause integrated into status

pause checkbox has been removed

* refactor: added cancelled status to the log doctype

1. Moved the status section to the bottom in parent doc
2. Using alerts to indicate Job trigger status

(cherry picked from commit ed14d1ce44)

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2023-04-25 19:20:06 +05:30
mergify[bot]
5cc3978c16 fix: pass reference_doctype in link queries (backport #35038) (#35039)
fix: pass reference_doctype in link queries (#35038)

(cherry picked from commit 6de71eb158)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-04-25 18:42:32 +05:30
mergify[bot]
5630e8189b fix: use filter_by_finance_book instead of only_depreciable_assets in fixed asset register (backport #35031) (#35035)
fix: use filter_by_finance_book instead of only_depreciable_assets in fixed asset register (#35031)

fix: use filter_by_finance_book instead of only_depreciable_assets
(cherry picked from commit e08d636bf7)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-04-25 15:53:10 +05:30
rohitwaghchaure
d121282439 Merge pull request #35016 from frappe/mergify/bp/version-14-hotfix/pr-35012
fix: item not showing in the BOM (backport #35012)
2023-04-25 14:49:35 +05:30
Anand Baburajan
3c75e55cb9 fix: value of depreciable assets not updating after manual depr entry [v14] (#35010)
* fix: update value of asset with calc_depr on after manual depr entry

* fix: value of asset with calc_depr on after manual depr entry not reflecting in asset_depr_and_bal report

* chore: add validation for depr journal entry

* test: manual_depr_for_depreciable_asset and manual_depr_w_incorrect_jv_voucher_type

* chore: unlink depreciable asset from manual depr entry
2023-04-25 12:44:58 +05:30
rohitwaghchaure
aa22cccf99 Merge pull request #35028 from frappe/mergify/bp/version-14-hotfix/pr-35026
Revert "fix: Incorrect difference value in Stock and Account Value Comparison…" (backport #35026)
2023-04-25 12:10:03 +05:30
rohitwaghchaure
7d9c9884dc Revert "fix: Incorrect difference value in Stock and Account Value Comparison…"
(cherry picked from commit 7a63fbef4f)
2023-04-25 06:39:36 +00:00
ruthra kumar
e03eaa31fe Merge pull request #35023 from frappe/mergify/bp/version-14-hotfix/pr-34974
Revert "fix: Rate from LDC in TDS reports" (backport #34974)
2023-04-25 12:04:50 +05:30
ruthra kumar
f44a79fa73 Revert "fix: Rate from LDC in TDS reports (#33699)"
This reverts commit db9beb3cdd.

(cherry picked from commit cb7a99cbaa)
2023-04-25 05:47:51 +00:00
Frappe PR Bot
943acbfea8 chore(release): Bumped to Version 14.22.3
## [14.22.3](https://github.com/frappe/erpnext/compare/v14.22.2...v14.22.3) (2023-04-24)

### Bug Fixes

* incorrect OR condition causing timeout error ([d5910fb](d5910fba44))
2023-04-24 12:41:30 +00:00
rohitwaghchaure
e955eeeabc Merge pull request #35018 from frappe/mergify/bp/version-14/pr-35017
fix: incorrect OR condition causing timeout error (For more than 50 line items) (backport #35015) (backport #35017)
2023-04-24 18:09:45 +05:30
Rohit Waghchaure
d5910fba44 fix: incorrect OR condition causing timeout error
(cherry picked from commit 379b215aea)
(cherry picked from commit c020789bfc)
2023-04-24 12:39:21 +00:00
rohitwaghchaure
c8622fb46f Merge pull request #35017 from frappe/mergify/bp/version-14-hotfix/pr-35015
fix: incorrect OR condition causing timeout error (For more than 50 line items) (backport #35015)
2023-04-24 18:08:34 +05:30
Rohit Waghchaure
c020789bfc fix: incorrect OR condition causing timeout error
(cherry picked from commit 379b215aea)
2023-04-24 12:37:50 +00:00
Rohit Waghchaure
28cd79a040 fix: item not showing in the BOM
(cherry picked from commit 02c3b41dc2)
2023-04-24 12:37:26 +00:00
mergify[bot]
70014028e9 chore: ERPNext setup wizard cleanup (#33675)
chore: ERPNext setup wizard cleanup (#33675)

* chore: ERPNext setup wizard cleanup

* chore: Remove default website

* chore: Remove flaky tests

* chore: remove unwanted tests

(cherry picked from commit 3598bcc9a8)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-04-24 15:28:41 +05:30
rohitwaghchaure
79cbe1cce8 Merge pull request #34995 from frappe/mergify/bp/version-14-hotfix/pr-34994
fix: duplicate reposting entries of same voucher (backport #34994)
2023-04-22 14:55:50 +05:30
Rohit Waghchaure
aeac43ccf9 fix: duplicate reposting entries of same voucher
(cherry picked from commit f2253dd645)
2023-04-22 08:38:53 +00:00
rohitwaghchaure
a5e6b371ac Merge pull request #34987 from frappe/mergify/bp/version-14-hotfix/pr-34982
fix: added validation for extra job cards (backport #34982)
2023-04-22 08:40:42 +05:30
mergify[bot]
83a1b836f9 fix: SLA permissions (backport #34981) (#34986)
fix: SLA permissions (#34981)

(cherry picked from commit ac871797b2)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-04-21 21:13:38 +05:30
rohitwaghchaure
6e03c09017 Merge pull request #34983 from frappe/mergify/bp/version-14-hotfix/pr-34980
fix: validation for internal transfer entry (backport #34980)
2023-04-21 18:53:41 +05:30
Rohit Waghchaure
a5fde5d933 fix: added validation for extra job card
(cherry picked from commit 6a0b7c9e8c)
2023-04-21 13:21:27 +00:00
Rohit Waghchaure
a5823547d3 fix: validation for internal transfer entry
(cherry picked from commit 19911b48fd)
2023-04-21 12:16:43 +00:00
mergify[bot]
00968badf5 fix: broken 'set exchange gain/loss' btn in payment entry (#34940)
fix: broken set exchagne gain/loss btn broken in payment entry

(cherry picked from commit df0682fa8c)

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2023-04-21 14:26:10 +05:30
mergify[bot]
6aabab26d8 fix: FEC report for France accountancy (#34781)
fix: FEC report for France accountancy  (#34781)

* fix: FEC report for France Accountancy legal requirement

* fix: FEC report for France Accountancy legal requirement

* fix: change to query standard

* fix: change to query standard

* fix: columns to standard dict

* fix: columns to standard dict

* fix: columns to data

* refactor: french report FEC

* refactor: french report FEC (2)

---------

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
(cherry picked from commit af8da53cf4)

Co-authored-by: HENRY Florian <florian.henry@open-concept.pro>
2023-04-21 14:23:18 +05:30
mergify[bot]
de86e8fb95 chore: Move source and campaign to more info section (#34946)
chore: Move source and campaign to more info section (#34946)

(cherry picked from commit a02705ded7)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-04-21 14:22:42 +05:30
ruthra kumar
e5048812fd Merge pull request #34943 from frappe/mergify/bp/version-14-hotfix/pr-34922
refactor: refactor set_missing_values and set_missing_ref_details in Payment Entry (backport #34922)
2023-04-21 09:41:28 +05:30
rohitwaghchaure
237bd6e831 Merge pull request #34964 from frappe/mergify/bp/version-14-hotfix/pr-34960
fix: stock entry type issue (backport #34960)
2023-04-20 23:41:46 +05:30
rohitwaghchaure
c2953bc3e0 Merge pull request #34965 from frappe/mergify/bp/version-14-hotfix/pr-34958
fix: removed depends on for the Employee Detail section (backport #34958)
2023-04-20 23:41:06 +05:30
Rohit Waghchaure
9d17d3ff06 fix: removed depends on for the Employee Detail section
(cherry picked from commit a90a5b4aa4)
2023-04-20 12:34:34 +00:00
Rohit Waghchaure
33a16086ef fix: stock entry type issue
(cherry picked from commit c3b5dcb767)
2023-04-20 12:34:11 +00:00
Sagar Vora
6e0d22c23b Merge pull request #34962 from frappe/mergify/bp/version-14-hotfix/pr-34738
fix: misc regional improvements (backport #34738)
2023-04-20 05:20:19 -07:00
Sagar Vora
2ec18eb4cf fix: use functools.wraps to preserve doc signature
(cherry picked from commit 776b56ccd1)
2023-04-20 12:18:21 +00:00
Sagar Vora
87595bdb7e fix: simplify erpnext.get_region
(cherry picked from commit 2fa641f86d)
2023-04-20 12:18:21 +00:00
Sagar Vora
789dfd6774 fix: set frappe.flags.company to call regional code accurately
(cherry picked from commit 17ef3c964f)
2023-04-20 12:18:20 +00:00
mergify[bot]
56ef0baa9d fix: batch qty conversion factor issue fixed in pos transaction (#34917)
fix: batch qty conversion factor issue fixed in pos transaction (#34917)

(cherry picked from commit 59f3fedbf7)

Co-authored-by: Vishal Dhayagude <vishdha@users.noreply.github.com>
Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2023-04-20 17:23:23 +05:30
Sagar Sharma
11e4fcb058 Merge pull request #34956 from frappe/mergify/bp/version-14-hotfix/pr-34953
fix: `PermissionError` in Work Order (backport #34953)
2023-04-20 16:01:47 +05:30
s-aga-r
a3568c1b27 fix: PermissionError in Work Order
(cherry picked from commit 8108b2de0a)
2023-04-20 10:28:04 +00:00
rohitwaghchaure
2a48a7b427 Merge pull request #34955 from frappe/mergify/bp/version-14-hotfix/pr-34954
fix: process_loss_percentage in BOM (backport #34954)
2023-04-20 15:02:05 +05:30
Nihantra Patel
4418862965 fix: process_loss_percentage in BOM
(cherry picked from commit b572bef71d)
2023-04-20 09:31:46 +00:00
rohitwaghchaure
e5aae90078 Merge pull request #34944 from frappe/mergify/bp/version-14-hotfix/pr-34912
fix: internal Purchase Receipt GL Entries (backport #34912)
2023-04-20 14:36:56 +05:30
Sagar Sharma
e2818afb62 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-34912 2023-04-20 14:05:48 +05:30
s-aga-r
7740ceb27e fix(test): test_internal_pr_gl_entries 2023-04-20 14:05:08 +05:30
ruthra kumar
3d3da75726 refactor: update ref details for selected references
set_missing_ref_details can update only for selected references

(cherry picked from commit b7d6e30f63)
2023-04-20 13:29:51 +05:30
ruthra kumar
8c5d644671 refactor: move set_missing_ref_detials out of set_missing_values
(cherry picked from commit 11cb2db3fe)
2023-04-20 13:29:47 +05:30
Frappe PR Bot
ca9b02fb53 chore(release): Bumped to Version 14.22.2
## [14.22.2](https://github.com/frappe/erpnext/compare/v14.22.1...v14.22.2) (2023-04-20)

### Bug Fixes

* filtering via batch no ([#34951](https://github.com/frappe/erpnext/issues/34951)) ([1b827e6](1b827e6b67)), closes [#34950](https://github.com/frappe/erpnext/issues/34950)
2023-04-20 07:24:45 +00:00
Deepesh Garg
74abe94711 Merge pull request #34952 from frappe/mergify/bp/version-14/pr-34951
fix: filtering via batch no (backport #34951)
2023-04-20 12:53:14 +05:30
mergify[bot]
1b827e6b67 fix: filtering via batch no (#34951)
fix: filtering via batch no(#34950)

* fix: filtering via batch no

(cherry picked from commit ea6eeace80)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit 3b23fc1eba)
2023-04-20 07:21:04 +00:00
mergify[bot]
3b23fc1eba fix: filtering via batch no (#34951)
fix: filtering via batch no(#34950)

* fix: filtering via batch no

(cherry picked from commit ea6eeace80)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-04-20 12:50:40 +05:30
Sagar Sharma
1743413e00 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-34912 2023-04-20 12:19:43 +05:30
Sagar Sharma
c11b98fa9f Merge pull request #34926 from frappe/mergify/bp/version-14-hotfix/pr-34895
fix: use `CombineDatetime` instead of `Timestamp` in QB queries (backport #34895)
2023-04-20 12:17:32 +05:30
Sagar Sharma
a2c25ed49e Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-34895 2023-04-20 12:17:12 +05:30
s-aga-r
2ad157bd77 fix(test): test_backdated_stock_reco_cancellation_future_negative_stock
(cherry picked from commit 11c8503180)
2023-04-20 06:06:05 +00:00
s-aga-r
7cc0129302 test: add test case for internal PR GL Entries
(cherry picked from commit c86c543fbf)
2023-04-20 06:06:05 +00:00
s-aga-r
f7e436fe71 fix: internal Purchase Receipt GL Entries
(cherry picked from commit 6fca9adcd4)
2023-04-20 06:06:05 +00:00
ruthra kumar
1ab83c5c02 Merge pull request #34916 from frappe/mergify/bp/version-14-hotfix/pr-34838
refactor: toggle merging similar ledger entries in JE (backport #34838)
2023-04-20 11:15:36 +05:30
mergify[bot]
7131ff28fd fix: add limit for get_next_stock_reco (#34937)
fix: limit stock reco issue

(cherry picked from commit fcfa8842a7)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2023-04-20 10:37:58 +05:30
Sagar Sharma
33d89b4a8b Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-34895 2023-04-20 10:05:58 +05:30
rohitwaghchaure
fef381e8eb Merge pull request #34930 from frappe/mergify/bp/version-14-hotfix/pr-34929
fix: Incorrect difference value in Stock and Account Value Comparison… (backport #34929)
2023-04-19 23:58:34 +05:30
Sagar Sharma
9e148b4277 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-34895 2023-04-19 21:21:52 +05:30
Rohit Waghchaure
e7ca833929 fix: Incorrect difference value in Stock and Account Value Comparison report
(cherry picked from commit a77182645f)
2023-04-19 15:38:16 +00:00
Sagar Sharma
322102e56b Merge pull request #34927 from frappe/mergify/bp/version-14-hotfix/pr-34901
fix: add item-code filter for SCR supplied-items batch-no (backport #34901)
2023-04-19 19:26:39 +05:30
s-aga-r
7c4f83ed60 fix: add item-code filter for SCR supplied-items batch-no
(cherry picked from commit e91abbfbe3)
2023-04-19 13:54:00 +00:00
s-aga-r
55da91cb34 fix: use CombineDatetime instead of Timestamp in QB queries
(cherry picked from commit 91a398a191)
2023-04-19 13:50:30 +00:00
ruthra kumar
9f090d2861 chore(patch): by default ledger entries of JE's will not be merged
(cherry picked from commit 3f537d30bd)
2023-04-19 09:59:36 +05:30
ruthra kumar
93bc1c5382 refactor: checkbox to toggle merging of JE account heads
(cherry picked from commit a3e3fe149d)
2023-04-19 04:19:31 +00:00
106 changed files with 2514 additions and 751 deletions

View File

@@ -1,8 +1,9 @@
import functools
import inspect
import frappe
__version__ = "14.22.1"
__version__ = "14.23.4"
def get_default_company(user=None):
@@ -120,12 +121,14 @@ def get_region(company=None):
You can also set global company flag in `frappe.flags.company`
"""
if company or frappe.flags.company:
return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
elif frappe.flags.country:
return frappe.flags.country
else:
return frappe.get_system_settings("country")
if not company:
company = frappe.local.flags.company
if company:
return frappe.get_cached_value("Company", company, "country")
return frappe.flags.country or frappe.get_system_settings("country")
def allow_regional(fn):
@@ -136,6 +139,7 @@ def allow_regional(fn):
def myfunction():
pass"""
@functools.wraps(fn)
def caller(*args, **kwargs):
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"

View File

@@ -297,7 +297,7 @@ def _make_test_records(verbose=None):
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None],
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],

View File

@@ -19,6 +19,8 @@
"column_break_17",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"journals_section",
"merge_similar_account_heads",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section",
@@ -38,6 +40,8 @@
"show_payment_schedule_in_print",
"currency_exchange_section",
"allow_stale",
"section_break_jpd0",
"auto_reconcile_payments",
"stale_days",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
@@ -168,11 +172,6 @@
"fieldtype": "Int",
"label": "Stale Days"
},
{
"fieldname": "report_settings_sb",
"fieldtype": "Section Break",
"label": "Report Settings"
},
{
"default": "0",
"description": "Only select this if you have set up the Cash Flow Mapper documents",
@@ -356,6 +355,29 @@
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
},
{
"fieldname": "journals_section",
"fieldtype": "Section Break",
"label": "Journals"
},
{
"default": "0",
"description": "Rows with Same Account heads will be merged on Ledger",
"fieldname": "merge_similar_account_heads",
"fieldtype": "Check",
"label": "Merge Similar Account Heads"
},
{
"fieldname": "section_break_jpd0",
"fieldtype": "Section Break",
"label": "Payment Reconciliations"
},
{
"default": "0",
"fieldname": "auto_reconcile_payments",
"fieldtype": "Check",
"label": "Auto Reconcile Payments"
}
],
"icon": "icon-cog",
@@ -363,7 +385,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-04-14 17:22:03.680886",
"modified": "2023-04-21 13:11:37.130743",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -56,7 +56,7 @@ class BankClearance(Document):
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount, 0) as credit,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency

View File

@@ -281,10 +281,13 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
)
elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
"sum(credit_in_account_currency)",
return abs(
frappe.db.get_value(
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
"sum(debit_in_account_currency-credit_in_account_currency)",
)
or 0
)
elif payment_entry.payment_document == "Expense Claim":

View File

@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement'];
},
refresh: function(frm) {

View File

@@ -69,6 +69,7 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table()
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
if self.docstatus == 0:
self.apply_tax_withholding()
@@ -130,6 +131,13 @@ class JournalEntry(AccountsController):
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
def validate_depr_entry_voucher_type(self):
if (
any(d.account_type == "Depreciation" for d in self.get("accounts"))
and self.voucher_type != "Depreciation Entry"
):
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
@@ -233,25 +241,30 @@ class JournalEntry(AccountsController):
self.remove(d)
def update_asset_value(self):
if self.voucher_type != "Depreciation Entry":
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if (
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and d.debit
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
continue
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
fb_idx = 1
if self.finance_book:
for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
fb_idx = fb_row.idx
break
fb_row = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation -= d.debit
fb_row.db_update()
else:
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
asset.set_status()
@@ -316,35 +329,35 @@ class JournalEntry(AccountsController):
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if (
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and d.debit
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
fb_idx = None
for s in asset.get("schedules"):
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
idx = cint(s.finance_book_id) or 1
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
asset.set_status()
fb_idx = cint(s.finance_book_id) or 1
break
if not fb_idx:
fb_idx = 1
if self.finance_book:
for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
fb_idx = fb_row.idx
break
fb_row = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation += d.debit
fb_row.db_update()
else:
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
asset.set_status()
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
asset.set_status()
def unlink_inter_company_jv(self):
if (
@@ -878,6 +891,8 @@ class JournalEntry(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries
merge_entries = frappe.db.get_single_value("Accounts Settings", "merge_similar_account_heads")
gl_map = self.build_gl_map()
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
update_outstanding = "No"
@@ -885,7 +900,13 @@ class JournalEntry(AccountsController):
update_outstanding = "Yes"
if gl_map:
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
make_gl_entries(
gl_map,
cancel=cancel,
adv_adj=adv_adj,
merge_entries=merge_entries,
update_outstanding=update_outstanding,
)
@frappe.whitelist()
def get_balance(self, difference_account=None):

View File

@@ -2,6 +2,21 @@
// For license information, please see license.txt
frappe.ui.form.on("Journal Entry Template", {
onload: function(frm) {
if(frm.is_new()) {
frappe.call({
type: "GET",
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
callback: function(r){
if(r.message) {
frm.set_df_property("naming_series", "options", r.message.split("\n"));
frm.set_value("naming_series", r.message.split("\n")[0]);
frm.refresh_field("naming_series");
}
}
});
}
},
refresh: function(frm) {
frappe.model.set_default_values(frm.doc);
@@ -19,18 +34,6 @@ frappe.ui.form.on("Journal Entry Template", {
return { filters: filters };
});
frappe.call({
type: "GET",
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
callback: function(r){
if(r.message){
frm.set_df_property("naming_series", "options", r.message.split("\n"));
frm.set_value("naming_series", r.message.split("\n")[0]);
frm.refresh_field("naming_series");
}
}
});
},
voucher_type: function(frm) {
var add_accounts = function(doc, r) {

View File

@@ -978,6 +978,7 @@ frappe.ui.form.on('Payment Entry', {
precision("difference_amount"));
const add_deductions = (details) => {
let row = null;
if (!write_off_row.length && difference_amount) {
row = frm.add_child("deductions");
row.account = details[account];

View File

@@ -60,6 +60,7 @@ class PaymentEntry(AccountsController):
def validate(self):
self.setup_party_account_field()
self.set_missing_values()
self.set_missing_ref_details()
self.validate_payment_type()
self.validate_party_details()
self.set_exchange_rate()
@@ -219,11 +220,16 @@ class PaymentEntry(AccountsController):
else self.paid_to_account_currency
)
self.set_missing_ref_details()
def set_missing_ref_details(self, force=False):
def set_missing_ref_details(
self, force: bool = False, update_ref_details_only_for: list | None = None
) -> None:
for d in self.get("references"):
if d.allocated_amount:
if update_ref_details_only_for and (
not (d.reference_doctype, d.reference_name) in update_ref_details_only_for
):
continue
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
@@ -648,6 +654,28 @@ class PaymentEntry(AccountsController):
self.precision("base_received_amount"),
)
def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes
exchange_rate = 1
if self.payment_type == "Receive":
exchange_rate = self.source_exchange_rate
elif self.payment_type == "Pay":
exchange_rate = self.target_exchange_rate
base_allocated_amount += flt(
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
)
else:
base_allocated_amount += flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
)
return base_allocated_amount
def set_total_allocated_amount(self):
if self.payment_type == "Internal Transfer":
return
@@ -656,9 +684,7 @@ class PaymentEntry(AccountsController):
for d in self.get("references"):
if d.allocated_amount:
total_allocated_amount += flt(d.allocated_amount)
base_total_allocated_amount += flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
)
base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
self.total_allocated_amount = abs(total_allocated_amount)
self.base_total_allocated_amount = abs(base_total_allocated_amount)
@@ -875,9 +901,7 @@ class PaymentEntry(AccountsController):
}
)
allocated_amount_in_company_currency = flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount")
)
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
gle.update(
{
@@ -1687,7 +1711,10 @@ def get_payment_entry(
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
100.0 + over_billing_allowance
):
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if not party_type:
@@ -1706,6 +1733,13 @@ def get_payment_entry(
# bank or cash
bank = get_bank_cash_account(doc, bank_account)
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
if party_type in ["Customer", "Supplier"] and not bank:
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
if party_bank_account:
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
bank = get_bank_cash_account(doc, account)
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
)
@@ -1811,6 +1845,7 @@ def get_payment_entry(
pe.setup_party_account_field()
pe.set_missing_values()
pe.set_missing_ref_details()
update_accounting_dimensions(pe, doc)
@@ -1921,19 +1956,27 @@ def set_paid_amount_and_received_amount(
paid_amount = received_amount = 0
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
elif payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
if payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
if company_currency != bank.account_currency:
received_amount = paid_amount / doc.get("conversion_rate", 1)
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get("conversion_rate", 1)
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
else:
if company_currency != bank.account_currency:
paid_amount = received_amount / doc.get("conversion_rate", 1)
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get("conversion_rate", 1)
return paid_amount, received_amount

View File

@@ -51,6 +51,38 @@ class TestPaymentEntry(FrappeTestCase):
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 0)
def test_payment_against_sales_order_usd_to_inr(self):
so = make_sales_order(
customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
)
so.conversion_rate = 50
so.submit()
pe = get_payment_entry("Sales Order", so.name)
pe.source_exchange_rate = 55
pe.received_amount = 5500
pe.insert()
pe.submit()
# there should be no difference amount
pe.reload()
self.assertEqual(pe.difference_amount, 0)
self.assertEqual(pe.deductions, [])
expected_gle = dict(
(d[0], d)
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]]
)
self.validate_gl_entries(pe.name, expected_gle)
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 100)
pe.cancel()
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 0)
def test_payment_entry_for_blocked_supplier_invoice(self):
supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1

View File

@@ -82,6 +82,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
this.frm.change_custom_button_type('Allocate', null, 'default');
}
// check for any running reconciliation jobs
if (this.frm.doc.receivable_payable_account) {
frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => {
if(enabled) {
this.frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
"args": {
for_filter: {
company: this.frm.doc.company,
party_type: this.frm.doc.party_type,
party: this.frm.doc.party,
receivable_payable_account: this.frm.doc.receivable_payable_account
}
}
}).then(r => {
if (r.message) {
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
this.frm.dashboard.add_comment(msg, "yellow");
}
});
}
});
}
}
company() {

View File

@@ -7,9 +7,12 @@ from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, getdate, nowdate, today
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
is_any_doc_running,
)
from erpnext.accounts.utils import (
QueryPaymentLedger,
get_outstanding_invoices,
@@ -304,9 +307,7 @@ class PaymentReconciliation(Document):
}
)
@frappe.whitelist()
def reconcile(self):
self.validate_allocation()
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@@ -330,12 +331,35 @@ class PaymentReconciliation(Document):
self.make_difference_entry(payment_details)
if entry_list:
reconcile_against_document(entry_list)
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
if dr_or_cr_notes:
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
@frappe.whitelist()
def reconcile(self):
if frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
running_doc = is_any_doc_running(
dict(
company=self.company,
party_type=self.party_type,
party=self.party,
receivable_payable_account=self.receivable_payable_account,
)
)
if running_doc:
frappe.throw(
_("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format(
get_link_to_form("Auto Reconcile", running_doc)
)
)
return
self.validate_allocation()
self.reconcile_allocations()
msgprint(_("Successfully Reconciled"))
self.get_unreconciled_entries()
def make_difference_entry(self, row):

View File

@@ -5,7 +5,7 @@
import unittest
import frappe
from frappe.utils import today
from frappe.utils import add_months, today
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
@@ -176,6 +176,23 @@ class TestPeriodClosingVoucher(unittest.TestCase):
)
self.assertSequenceEqual(pcv_gle, expected_gle)
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
repost_doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"company": company,
"posting_date": "2020-03-15",
"based_on": "Item and Warehouse",
"item_code": "Test Item 1",
"warehouse": warehouse,
}
)
self.assertRaises(frappe.ValidationError, repost_doc.save)
repost_doc.posting_date = add_months(today(), 13)
repost_doc.save()
def make_period_closing_voucher(self, submit=True):
surplus_account = create_account()

View File

@@ -0,0 +1,130 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Process Payment Reconciliation", {
onload: function(frm) {
// set queries
frm.set_query("party_type", function() {
return {
"filters": {
"name": ["in", Object.keys(frappe.boot.party_account_types)],
}
}
});
frm.set_query('receivable_payable_account', function(doc) {
return {
filters: {
"company": doc.company,
"is_group": 0,
"account_type": frappe.boot.party_account_types[doc.party_type]
}
};
});
frm.set_query('cost_center', function(doc) {
return {
filters: {
"company": doc.company,
"is_group": 0,
}
};
});
frm.set_query('bank_cash_account', function(doc) {
return {
filters:[
['Account', 'company', '=', doc.company],
['Account', 'is_group', '=', 0],
['Account', 'account_type', 'in', ['Bank', 'Cash']]
]
};
});
},
refresh: function(frm) {
if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) {
let execute_btn = __("Start / Resume")
frm.add_custom_button(execute_btn, () => {
frm.call({
method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc',
args: {
docname: frm.doc.name
}
}).then(r => {
if(!r.exc) {
frappe.show_alert(__("Job Started"));
frm.reload_doc();
}
});
});
}
if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count",
args: {
"docname": frm.docname,
}
}).then(r => {
if (r.message) {
let progress = 0;
let description = "";
if (r.message.processed) {
progress = (r.message.processed/r.message.total) * 100;
description = r.message.processed + "/" + r.message.total + " processed";
} else if (r.message.total == 0 && frm.doc.status == "Completed") {
progress = 100;
}
frm.dashboard.add_progress('Reconciliation Progress', progress, description);
}
})
}
if (frm.doc.docstatus==1 && frm.doc.status == 'Running') {
let execute_btn = __("Pause")
frm.add_custom_button(execute_btn, () => {
frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc",
args: {
"docname": frm.docname,
}
}).then(r => {
if (!r.exc) {
frappe.show_alert(__("Job Paused"));
frm.reload_doc()
}
});
});
}
},
company(frm) {
frm.set_value('party', '');
frm.set_value('receivable_payable_account', '');
},
party_type(frm) {
frm.set_value('party', '');
},
party(frm) {
frm.set_value('receivable_payable_account', '');
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
return frappe.call({
method: "erpnext.accounts.party.get_party_account",
args: {
company: frm.doc.company,
party_type: frm.doc.party_type,
party: frm.doc.party
},
callback: (r) => {
if (!r.exc && r.message) {
frm.set_value("receivable_payable_account", r.message);
}
frm.refresh();
}
});
}
}
});

View File

@@ -0,0 +1,173 @@
{
"actions": [],
"autoname": "format:ACC-PPR-{#####}",
"beta": 1,
"creation": "2023-03-30 21:28:39.793927",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"party_type",
"column_break_io6c",
"party",
"receivable_payable_account",
"filter_section",
"from_invoice_date",
"to_invoice_date",
"column_break_kegk",
"from_payment_date",
"to_payment_date",
"column_break_uj04",
"cost_center",
"bank_cash_account",
"section_break_2n02",
"status",
"error_log",
"section_break_a8yx",
"amended_from"
],
"fields": [
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nQueued\nRunning\nPaused\nCompleted\nPartially Reconciled\nFailed\nCancelled",
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "party_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Party Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "column_break_io6c",
"fieldtype": "Column Break"
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Party",
"options": "party_type",
"reqd": 1
},
{
"fieldname": "receivable_payable_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Receivable/Payable Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "filter_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "from_invoice_date",
"fieldtype": "Date",
"label": "From Invoice Date"
},
{
"fieldname": "to_invoice_date",
"fieldtype": "Date",
"label": "To Invoice Date"
},
{
"fieldname": "column_break_kegk",
"fieldtype": "Column Break"
},
{
"fieldname": "from_payment_date",
"fieldtype": "Date",
"label": "From Payment Date"
},
{
"fieldname": "to_payment_date",
"fieldtype": "Date",
"label": "To Payment Date"
},
{
"fieldname": "column_break_uj04",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "bank_cash_account",
"fieldtype": "Link",
"label": "Bank/Cash Account",
"options": "Account"
},
{
"fieldname": "section_break_2n02",
"fieldtype": "Section Break",
"label": "Status"
},
{
"depends_on": "eval:doc.error_log",
"fieldname": "error_log",
"fieldtype": "Long Text",
"label": "Error Log"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Process Payment Reconciliation",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_a8yx",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-04-21 17:19:30.912953",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "company"
}

View File

@@ -0,0 +1,503 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.utils import get_link_to_form
from frappe.utils.scheduler import is_scheduler_inactive
class ProcessPaymentReconciliation(Document):
def validate(self):
self.validate_receivable_payable_account()
self.validate_bank_cash_account()
def validate_receivable_payable_account(self):
if self.receivable_payable_account:
if self.company != frappe.db.get_value("Account", self.receivable_payable_account, "company"):
frappe.throw(
_("Receivable/Payable Account: {0} doesn't belong to company {1}").format(
frappe.bold(self.receivable_payable_account), frappe.bold(self.company)
)
)
def validate_bank_cash_account(self):
if self.bank_cash_account:
if self.company != frappe.db.get_value("Account", self.bank_cash_account, "company"):
frappe.throw(
_("Bank/Cash Account {0} doesn't belong to company {1}").format(
frappe.bold(self.bank_cash_account), frappe.bold(self.company)
)
)
def before_save(self):
self.status = ""
self.error_log = ""
def on_submit(self):
self.db_set("status", "Queued")
self.db_set("error_log", None)
def on_cancel(self):
self.db_set("status", "Cancelled")
log = frappe.db.get_value(
"Process Payment Reconciliation Log", filters={"process_pr": self.name}
)
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled")
@frappe.whitelist()
def get_reconciled_count(docname: str | None = None) -> float:
current_status = {}
if docname:
reconcile_log = frappe.db.get_value(
"Process Payment Reconciliation Log", filters={"process_pr": docname}, fieldname="name"
)
if reconcile_log:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": reconcile_log},
fields=["reconciled_entries", "total_allocations"],
as_list=1,
)
current_status["processed"], current_status["total"] = res[0]
return current_status
def get_pr_instance(doc: str):
process_payment_reconciliation = frappe.get_doc("Process Payment Reconciliation", doc)
pr = frappe.get_doc("Payment Reconciliation")
fields = [
"company",
"party_type",
"party",
"receivable_payable_account",
"from_invoice_date",
"to_invoice_date",
"from_payment_date",
"to_payment_date",
]
d = {}
for field in fields:
d[field] = process_payment_reconciliation.get(field)
pr.update(d)
pr.invoice_limit = 1000
pr.payment_limit = 1000
return pr
def is_job_running(job_name: str) -> bool:
jobs = frappe.db.get_all("RQ Job", filters={"status": ["in", ["started", "queued"]]})
for x in jobs:
if x.job_name == job_name:
return True
return False
@frappe.whitelist()
def pause_job_for_doc(docname: str | None = None):
if docname:
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Paused")
@frappe.whitelist()
def trigger_job_for_doc(docname: str | None = None):
"""
Trigger background job
"""
if not docname:
return
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
get_link_to_form("Accounts Settings", "Accounts Settings")
)
)
return
if not is_scheduler_inactive():
if frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Queued":
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
job_name = f"start_processing_{docname}"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
queue="long",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=docname,
)
elif frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Paused":
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Running")
# Resume tasks for running doc
job_name = f"start_processing_{docname}"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
queue="long",
is_async=True,
job_name=job_name,
doc=docname,
)
else:
frappe.msgprint(_("Scheduler is Inactive. Can't trigger job now."))
def trigger_reconciliation_for_queued_docs():
"""
Will be called from Cron Job
Fetch queued docs and start reconciliation process for each one
"""
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
get_link_to_form("Accounts Settings", "Accounts Settings")
)
)
return
if not is_scheduler_inactive():
# Get all queued documents
all_queued = frappe.db.get_all(
"Process Payment Reconciliation",
filters={"docstatus": 1, "status": "Queued"},
order_by="creation desc",
as_list=1,
)
docs_to_trigger = []
unique_filters = set()
queue_size = 5
fields = ["company", "party_type", "party", "receivable_payable_account"]
def get_filters_as_tuple(fields, doc):
filters = ()
for x in fields:
filters += tuple(doc.get(x))
return filters
for x in all_queued:
doc = frappe.get_doc("Process Payment Reconciliation", x)
filters = get_filters_as_tuple(fields, doc)
if filters not in unique_filters:
unique_filters.add(filters)
docs_to_trigger.append(doc.name)
if len(docs_to_trigger) == queue_size:
break
# trigger reconcilation process for queue_size unique filters
for doc in docs_to_trigger:
trigger_job_for_doc(doc)
else:
frappe.msgprint(_("Scheduler is Inactive. Can't trigger jobs now."))
def reconcile_based_on_filters(doc: None | str = None) -> None:
"""
Identify current state of document and execute next tasks in background
"""
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if not log:
log = frappe.new_doc("Process Payment Reconciliation Log")
log.process_pr = doc
log.status = "Running"
log = log.save()
job_name = f"process_{doc}_fetch_and_allocate"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
queue="long",
timeout="3600",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=doc,
)
else:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": log},
fields=["allocated", "reconciled"],
as_list=1,
)
allocated, reconciled = res[0]
if not allocated:
job_name = f"process__{doc}_fetch_and_allocate"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
queue="long",
timeout="3600",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=doc,
)
elif not reconciled:
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
elif reconciled:
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
def get_next_allocation(log: str) -> list:
if log:
allocations = []
next = frappe.db.get_all(
"Process Payment Reconciliation Log Allocations",
filters={"parent": log, "reconciled": 0},
fields=["reference_type", "reference_name"],
order_by="idx",
limit=1,
)
if next:
allocations = frappe.db.get_all(
"Process Payment Reconciliation Log Allocations",
filters={
"parent": log,
"reconciled": 0,
"reference_type": next[0].reference_type,
"reference_name": next[0].reference_name,
},
fields=["*"],
order_by="idx",
)
return allocations
return []
def fetch_and_allocate(doc: str) -> None:
"""
Fetch Invoices and Payments based on filters applied. FIFO ordering is used for allocation.
"""
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if log:
if not frappe.db.get_value("Process Payment Reconciliation Log", log, "allocated"):
reconcile_log = frappe.get_doc("Process Payment Reconciliation Log", log)
pr = get_pr_instance(doc)
pr.get_unreconciled_entries()
if len(pr.invoices) > 0 and len(pr.payments) > 0:
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
for x in pr.get("allocation"):
reconcile_log.append(
"allocations",
x.as_dict().update(
{
"parenttype": "Process Payment Reconciliation Log",
"parent": reconcile_log.name,
"name": None,
"reconciled": False,
}
),
)
reconcile_log.allocated = True
reconcile_log.total_allocations = len(reconcile_log.get("allocations"))
reconcile_log.reconciled_entries = 0
reconcile_log.save()
# generate reconcile job name
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
def reconcile(doc: None | str = None) -> None:
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if log:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": log},
fields=["reconciled_entries", "total_allocations"],
as_list=1,
limit=1,
)
reconciled_entries, total_allocations = res[0]
if reconciled_entries != total_allocations:
try:
# Fetch next allocation
allocations = get_next_allocation(log)
pr = get_pr_instance(doc)
# pass allocation to PR instance
for x in allocations:
pr.append("allocation", x)
# reconcile
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
# If Payment Entry, update details only for newly linked references
# This is for performance
if allocations[0].reference_type == "Payment Entry":
references = [(x.invoice_type, x.invoice_number) for x in allocations]
pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name)
pe.flags.ignore_validate_update_after_submit = True
pe.set_missing_ref_details(update_ref_details_only_for=references)
pe.save()
# Update reconciled flag
allocation_names = [x.name for x in allocations]
ppa = qb.DocType("Process Payment Reconciliation Log Allocations")
qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run()
# Update reconciled count
reconciled_count = frappe.db.count(
"Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True}
)
frappe.db.set_value(
"Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count
)
except Exception as err:
# Update the parent doc about the exception
frappe.db.rollback()
traceback = frappe.get_traceback()
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"error_log",
message,
)
if reconciled_entries and total_allocations and reconciled_entries < total_allocations:
frappe.db.set_value(
"Process Payment Reconciliation Log", log, "status", "Partially Reconciled"
)
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"status",
"Partially Reconciled",
)
else:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Failed")
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"status",
"Failed",
)
finally:
if reconciled_entries == total_allocations:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
else:
if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"):
# trigger next batch in job
# generate reconcile job name
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
else:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
@frappe.whitelist()
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None
if for_filter:
if type(for_filter) == str:
for_filter = frappe.json.loads(for_filter)
running_doc = frappe.db.get_value(
"Process Payment Reconciliation",
filters={
"docstatus": 1,
"status": ["in", ["Running", "Paused"]],
"company": for_filter.get("company"),
"party_type": for_filter.get("party_type"),
"party": for_filter.get("party"),
"receivable_payable_account": for_filter.get("receivable_payable_account"),
},
fieldname="name",
)
else:
running_doc = frappe.db.get_value(
"Process Payment Reconciliation", filters={"docstatus": 1, "status": "Running"}
)
return running_doc

View File

@@ -0,0 +1,15 @@
from frappe import _
def get_data():
return {
"fieldname": "process_pr",
"transactions": [
{
"label": _("Reconciliation Logs"),
"items": [
"Process Payment Reconciliation Log",
],
},
],
}

View File

@@ -0,0 +1,15 @@
frappe.listview_settings['Process Payment Reconciliation'] = {
add_fields: ["status"],
get_indicator: function(doc) {
let colors = {
'Queued': 'orange',
'Paused': 'orange',
'Completed': 'green',
'Partially Reconciled': 'orange',
'Running': 'blue',
'Failed': 'red',
};
let status = doc.status;
return [__(status), colors[status], 'status,=,'+status];
},
};

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestProcessPaymentReconciliation(FrappeTestCase):
pass

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Process Payment Reconciliation Log", {
refresh(frm) {
if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
let progress = 0;
if (frm.doc.reconciled_entries != 0) {
progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100;
} else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){
progress = 100;
}
frm.dashboard.add_progress(__('Reconciliation Progress'), progress);
}
},
});

View File

@@ -0,0 +1,137 @@
{
"actions": [],
"autoname": "format:PPR-LOG-{##}",
"beta": 1,
"creation": "2023-03-13 15:00:09.149681",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"process_pr",
"section_break_fvdw",
"status",
"tasks_section",
"allocated",
"reconciled",
"column_break_yhin",
"total_allocations",
"reconciled_entries",
"section_break_4ywv",
"error_log",
"allocations_section",
"allocations"
],
"fields": [
{
"fieldname": "allocations",
"fieldtype": "Table",
"label": "Allocations",
"options": "Process Payment Reconciliation Log Allocations",
"read_only": 1
},
{
"default": "0",
"description": "All allocations have been successfully reconciled",
"fieldname": "reconciled",
"fieldtype": "Check",
"label": "Reconciled",
"read_only": 1
},
{
"fieldname": "total_allocations",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Total Allocations",
"read_only": 1
},
{
"default": "0",
"description": "Invoices and Payments have been Fetched and Allocated",
"fieldname": "allocated",
"fieldtype": "Check",
"label": "Allocated",
"read_only": 1
},
{
"fieldname": "reconciled_entries",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Reconciled Entries",
"read_only": 1
},
{
"fieldname": "tasks_section",
"fieldtype": "Section Break",
"label": "Tasks"
},
{
"fieldname": "allocations_section",
"fieldtype": "Section Break",
"label": "Allocations"
},
{
"fieldname": "column_break_yhin",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4ywv",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.error_log",
"fieldname": "error_log",
"fieldtype": "Long Text",
"label": "Reconciliation Error Log",
"read_only": 1
},
{
"fieldname": "process_pr",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Parent Document",
"options": "Process Payment Reconciliation",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_fvdw",
"fieldtype": "Section Break",
"label": "Status"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Running\nPaused\nReconciled\nPartially Reconciled\nFailed\nCancelled",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-04-21 17:36:26.642617",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "allocated, reconciled, total_allocations, reconciled_entries",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ProcessPaymentReconciliationLog(Document):
pass

View File

@@ -0,0 +1,15 @@
frappe.listview_settings['Process Payment Reconciliation Log'] = {
add_fields: ["status"],
get_indicator: function(doc) {
var colors = {
'Partially Reconciled': 'orange',
'Paused': 'orange',
'Reconciled': 'green',
'Failed': 'red',
'Cancelled': 'red',
'Running': 'blue',
};
let status = doc.status;
return [__(status), colors[status], "status,=,"+status];
},
};

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestProcessPaymentReconciliationLog(FrappeTestCase):
pass

View File

@@ -0,0 +1,170 @@
{
"actions": [],
"creation": "2023-03-13 13:51:27.351463",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"reference_row",
"column_break_3",
"invoice_type",
"invoice_number",
"section_break_6",
"allocated_amount",
"unreconciled_amount",
"column_break_8",
"amount",
"is_advance",
"section_break_5",
"difference_amount",
"column_break_7",
"difference_account",
"exchange_rate",
"currency",
"reconciled"
],
"fields": [
{
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"options": "reference_type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Row",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "invoice_type",
"fieldtype": "Link",
"label": "Invoice Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "invoice_number",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Invoice Number",
"options": "invoice_type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "currency",
"reqd": 1
},
{
"fieldname": "unreconciled_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Unreconciled Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "is_advance",
"fieldtype": "Data",
"hidden": 1,
"label": "Is Advance",
"read_only": 1
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "difference_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Difference Amount",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "difference_account",
"fieldtype": "Link",
"label": "Difference Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"default": "0",
"fieldname": "reconciled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Reconciled"
}
],
"istable": 1,
"links": [],
"modified": "2023-03-20 21:05:43.121945",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log Allocations",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ProcessPaymentReconciliationLogAllocations(Document):
pass

View File

@@ -15,7 +15,12 @@
</div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
{% if filters.party[0] == filters.party_name[0] %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
{% else %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party[0] }}</b></h5>
<h5 style="float: left; margin-left:15px">{{ _("Customer Name: ") }} <b>{{filters.party_name[0] }}</b></h5>
{% endif %}
<h5 style="float: right;">
{{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}}

View File

@@ -34,6 +34,8 @@
"terms_and_conditions",
"section_break_1",
"enable_auto_email",
"column_break_ocfq",
"sender",
"section_break_18",
"frequency",
"filter_duration",
@@ -284,10 +286,32 @@
"fieldtype": "Link",
"label": "Terms and Conditions",
"options": "Terms and Conditions"
},
{
"default": "1",
"fieldname": "include_break",
"fieldtype": "Check",
"label": "Page Break After Each SoA"
},
{
"default": "0",
"fieldname": "show_net_values_in_party_account",
"fieldtype": "Check",
"label": "Show Net Values in Party Account"
},
{
"fieldname": "sender",
"fieldtype": "Link",
"label": "Sender",
"options": "Email Account"
},
{
"fieldname": "column_break_ocfq",
"fieldtype": "Column Break"
}
],
"links": [],
"modified": "2021-09-06 21:00:45.732505",
"modified": "2023-04-26 12:46:43.645455",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -327,7 +327,7 @@ def send_emails(document_name, from_scheduler=False):
queue="short",
method=frappe.sendmail,
recipients=recipients,
sender=frappe.session.user,
sender=doc.sender or frappe.session.user,
cc=cc,
subject=subject,
message=message,

View File

@@ -27,7 +27,7 @@
},
{
"fieldname": "billing_email",
"fieldtype": "Read Only",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Billing Email"
},
@@ -41,7 +41,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-03-13 00:12:34.508086",
"modified": "2023-04-26 13:02:41.964499",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts Customer",

View File

@@ -89,6 +89,7 @@
"column_break8",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance",
@@ -1559,13 +1560,19 @@
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
},
{
"default": "0",
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company Default Round Off Cost Center"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-04-03 22:57:14.074982",
"modified": "2023-04-28 12:57:50.832598",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -978,7 +978,7 @@ class PurchaseInvoice(BuyingController):
def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
precision_loss = self.get("base_net_total") - flt(
@@ -992,7 +992,9 @@ class PurchaseInvoice(BuyingController):
"account": round_off_account,
"against": self.supplier,
"credit": precision_loss,
"cost_center": self.cost_center or round_off_cost_center,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else self.cost_center or round_off_cost_center,
"remarks": _("Net total calculation precision loss"),
}
)
@@ -1386,7 +1388,7 @@ class PurchaseInvoice(BuyingController):
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
gl_entries.append(
@@ -1396,7 +1398,9 @@ class PurchaseInvoice(BuyingController):
"against": self.supplier,
"debit_in_account_currency": self.rounding_adjustment,
"debit": self.base_rounding_adjustment,
"cost_center": self.cost_center or round_off_cost_center,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else (self.cost_center or round_off_cost_center),
},
item=self,
)

View File

@@ -334,6 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
make_inter_company_invoice() {
let me = this;
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
frm: me.frm

View File

@@ -79,6 +79,7 @@
"column_break5",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance",
@@ -2134,6 +2135,12 @@
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
},
{
"default": "0",
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company default Cost Center for Round off"
}
],
"icon": "fa fa-file-text",
@@ -2146,7 +2153,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-04-03 22:55:14.206473",
"modified": "2023-04-28 14:15:59.901154",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -1450,7 +1450,7 @@ class SalesInvoice(SellingController):
and not self.is_internal_transfer()
):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Sales Invoice", self.name
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
)
gl_entries.append(
@@ -1462,7 +1462,9 @@ class SalesInvoice(SellingController):
self.rounding_adjustment, self.precision("rounding_adjustment")
),
"credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")),
"cost_center": self.cost_center or round_off_cost_center,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else (self.cost_center or round_off_cost_center),
},
item=self,
)

View File

@@ -215,7 +215,7 @@ def get_tax_row_for_tds(tax_details, tax_amount):
}
def get_lower_deduction_certificate(tax_details, pan_no):
def get_lower_deduction_certificate(company, tax_details, pan_no):
ldc_name = frappe.db.get_value(
"Lower Deduction Certificate",
{
@@ -223,6 +223,7 @@ def get_lower_deduction_certificate(tax_details, pan_no):
"tax_withholding_category": tax_details.tax_withholding_category,
"valid_from": (">=", tax_details.from_date),
"valid_upto": ("<=", tax_details.to_date),
"company": company,
},
"name",
)
@@ -255,7 +256,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
if party_type == "Supplier":
ldc = get_lower_deduction_certificate(tax_details, pan_no)
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
if tax_deducted:
net_total = inv.tax_withholding_net_total
if ldc:

View File

@@ -472,7 +472,9 @@ def update_accounting_dimensions(round_off_gle):
round_off_gle[dimension] = dimension_values.get(dimension)
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
def get_round_off_account_and_cost_center(
company, voucher_type, voucher_no, use_company_default=False
):
round_off_account, round_off_cost_center = frappe.get_cached_value(
"Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None]
@@ -480,7 +482,7 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE
if meta.has_field("cost_center"):
if not use_company_default and meta.has_field("cost_center"):
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
if parent_cost_center:
round_off_cost_center = parent_cost_center

View File

@@ -259,6 +259,8 @@ def set_address_details(
)
if doctype in TRANSACTION_TYPES:
# required to set correct region
frappe.flags.company = company
get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address

View File

@@ -114,28 +114,6 @@ def get_assets(filters):
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
ifnull(sum(case when ds.schedule_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
ds.depreciation_amount
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and ds.schedule_date <= a.disposal_date then
ds.depreciation_amount
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
ds.depreciation_amount
else
0
end), 0) as depreciation_amount_during_the_period
from `tabAsset` a, `tabDepreciation Schedule` ds
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
@@ -160,7 +138,7 @@ def get_assets(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
group by a.asset_category
union
SELECT a.asset_category,

View File

@@ -80,7 +80,7 @@ def get_entries(filters):
payment_entries = frappe.db.sql(
"""SELECT
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
if(paid_from=%(account)s, paid_amount * -1, received_amount)
if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
FROM
`tabPayment Entry`
WHERE

View File

@@ -492,11 +492,26 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions.append("cost_center in %(cost_center)s")
if filters.get("include_default_book_entries"):
additional_conditions.append(
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
if filters.get("finance_book"):
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
filters.get("company_fb")
):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
else:
additional_conditions.append(
"(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
)
else:
additional_conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)")
else:
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
if filters.get("finance_book"):
additional_conditions.append(
"(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
)
else:
additional_conditions.append("(finance_book in ('') OR finance_book IS NULL)")
if accounting_dimensions:
for dimension in accounting_dimensions:

View File

@@ -176,7 +176,8 @@ frappe.query_reports["General Ledger"] = {
{
"fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"),
"fieldtype": "Check"
"fieldtype": "Check",
"default": 1
},
{
"fieldname": "show_cancelled_entries",

View File

@@ -244,13 +244,23 @@ def get_conditions(filters):
if filters.get("project"):
conditions.append("project in %(project)s")
if filters.get("finance_book"):
if filters.get("include_default_book_entries"):
conditions.append(
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
if filters.get("include_default_book_entries"):
if filters.get("finance_book"):
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
filters.get("company_fb")
):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
else:
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
else:
conditions.append("finance_book in (%(finance_book)s)")
conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)")
else:
if filters.get("finance_book"):
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
else:
conditions.append("(finance_book in ('') OR finance_book IS NULL)")
if not filters.get("show_cancelled_entries"):
conditions.append("is_cancelled = 0")

View File

@@ -59,7 +59,7 @@ frappe.query_reports["Gross Profit"] = {
if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
column._options = "Sales Invoice";
} else {
column._options = "Item";
column._options = "";
}
value = default_formatter(value, row, column, data);

View File

@@ -250,7 +250,7 @@ def get_columns(group_wise_columns, filters):
"label": _("Warehouse"),
"fieldname": "warehouse",
"fieldtype": "Link",
"options": "warehouse",
"options": "Warehouse",
"width": 100,
},
"qty": {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80},
@@ -305,7 +305,8 @@ def get_columns(group_wise_columns, filters):
"sales_person": {
"label": _("Sales Person"),
"fieldname": "sales_person",
"fieldtype": "Data",
"fieldtype": "Link",
"options": "Sales Person",
"width": 100,
},
"allocated_amount": {
@@ -326,14 +327,14 @@ def get_columns(group_wise_columns, filters):
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "customer",
"options": "Customer Group",
"width": 100,
},
"territory": {
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "territory",
"options": "Territory",
"width": 100,
},
"monthly": {

View File

@@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.utils import flt
def execute(filters=None):
@@ -66,12 +65,6 @@ def get_result(
else:
total_amount_credited += entry.credit
## Check if ldc is applied and show rate as per ldc
actual_rate = (tds_deducted / total_amount_credited) * 100
if flt(actual_rate) < flt(rate):
rate = actual_rate
if tds_deducted:
row = {
"pan"

View File

@@ -157,12 +157,25 @@ def get_rootwise_opening_balances(filters, report_type):
if filters.project:
additional_conditions += " and project = %(project)s"
company_fb = frappe.db.get_value("Company", filters.company, "default_finance_book")
if filters.get("include_default_book_entries"):
additional_conditions += (
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
if filters.get("finance_book"):
if company_fb and cstr(filters.get("finance_book")) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
else:
additional_conditions += (
" AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
)
else:
additional_conditions += " AND (finance_book in (%(company_fb)s, '') OR finance_book IS NULL)"
else:
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
if filters.get("finance_book"):
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
else:
additional_conditions += " AND (finance_book in ('') OR finance_book IS NULL)"
accounting_dimensions = get_accounting_dimensions(as_list=False)
@@ -174,7 +187,7 @@ def get_rootwise_opening_balances(filters, report_type):
"year_start_date": filters.year_start_date,
"project": filters.project,
"finance_book": filters.finance_book,
"company_fb": frappe.db.get_value("Company", filters.company, "default_finance_book"),
"company_fb": company_fb,
}
if accounting_dimensions:

View File

@@ -420,7 +420,7 @@ def add_cc(args=None):
return cc.name
def reconcile_against_document(args): # nosemgrep
def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
"""
Cancel PE or JV, Update against document, split if required and resubmit
"""
@@ -449,7 +449,9 @@ def reconcile_against_document(args): # nosemgrep
if voucher_type == "Journal Entry":
update_reference_in_journal_entry(entry, doc, do_not_save=True)
else:
update_reference_in_payment_entry(entry, doc, do_not_save=True)
update_reference_in_payment_entry(
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
)
doc.save(ignore_permissions=True)
# re-submit advance entry
@@ -586,7 +588,9 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
journal_entry.save(ignore_permissions=True)
def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
def update_reference_in_payment_entry(
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
):
reference_details = {
"reference_doctype": d.against_voucher_type,
"reference_name": d.against_voucher,
@@ -632,6 +636,13 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.set_gain_or_loss(account_details=account_details)
payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details()
payment_entry.set_amounts()
if not do_not_save:
payment_entry.save(ignore_permissions=True)
@@ -1352,10 +1363,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
if wh_details.account == account and not wh_details.is_group
]
total_stock_value = 0.0
for warehouse in related_warehouses:
value = get_stock_value_on(warehouse, posting_date)
total_stock_value += value
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses

View File

@@ -126,6 +126,7 @@ def make_depreciation_entry(asset_name, date=None):
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
je.flags.planned_depr_entry = True
je.save()
if not je.meta.get_workflow():
je.submit()

View File

@@ -1453,7 +1453,7 @@ class TestDepreciationBasics(AssetSetup):
)
self.assertEqual(asset.status, "Submitted")
self.assertEqual(asset.get("value_after_depreciation"), 100000)
self.assertEqual(asset.get_value_after_depreciation(), 100000)
jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
@@ -1466,12 +1466,68 @@ class TestDepreciationBasics(AssetSetup):
jv.submit()
asset.reload()
self.assertEqual(asset.get("value_after_depreciation"), 99900)
self.assertEqual(asset.get_value_after_depreciation(), 99900)
jv.cancel()
asset.reload()
self.assertEqual(asset.get("value_after_depreciation"), 100000)
self.assertEqual(asset.get_value_after_depreciation(), 100000)
def test_manual_depreciation_for_depreciable_asset(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
purchase_date="2020-01-30",
available_for_use_date="2020-01-30",
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
self.assertEqual(asset.status, "Submitted")
self.assertEqual(asset.get_value_after_depreciation(), 100000)
jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
)
for d in jv.accounts:
d.reference_type = "Asset"
d.reference_name = asset.name
jv.voucher_type = "Depreciation Entry"
jv.insert()
jv.submit()
asset.reload()
self.assertEqual(asset.get_value_after_depreciation(), 99900)
jv.cancel()
asset.reload()
self.assertEqual(asset.get_value_after_depreciation(), 100000)
def test_manual_depreciation_with_incorrect_jv_voucher_type(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
purchase_date="2020-01-30",
available_for_use_date="2020-01-30",
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
)
for d in jv.accounts:
d.reference_type = "Asset"
d.reference_name = asset.name
d.account_type = "Depreciation"
jv.voucher_type = "Journal Entry"
self.assertRaises(frappe.ValidationError, jv.insert)
def create_asset_data():

View File

@@ -116,7 +116,9 @@ class AssetValueAdjustment(Document):
if d.depreciation_method in ("Straight Line", "Manual"):
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
total_days = date_diff(end_date, self.date)
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
total_days
)
from_date = self.date
else:
no_of_depreciations = len(

View File

@@ -94,11 +94,11 @@ frappe.query_reports["Fixed Asset Register"] = {
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book",
depends_on: "eval: doc.only_depreciable_assets == 1",
depends_on: "eval: doc.filter_by_finance_book == 1",
},
{
fieldname:"only_depreciable_assets",
label: __("Only depreciable assets"),
fieldname:"filter_by_finance_book",
label: __("Filter by Finance Book"),
fieldtype: "Check"
},
{

View File

@@ -45,8 +45,6 @@ def get_conditions(filters):
filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("only_depreciable_assets"):
conditions["calculate_depreciation"] = filters.get("only_depreciable_assets")
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"):
@@ -106,7 +104,7 @@ def get_data(filters):
assets_linked_to_fb = None
if filters.only_depreciable_assets:
if filters.filter_by_finance_book:
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},

View File

@@ -43,7 +43,7 @@ frappe.listview_settings['Purchase Order'] = {
});
listview.page.add_action_item(__("Advance Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
});
}

View File

@@ -5,7 +5,7 @@
import json
import frappe
from frappe import _, throw
from frappe import _, bold, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import (
@@ -405,6 +405,15 @@ class AccountsController(TransactionBase):
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
field = frappe.scrub(label)
for row in self.get("items"):
if not row.get(field):
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1
@@ -1653,7 +1662,10 @@ class AccountsController(TransactionBase):
)
self.append("payment_schedule", data)
if not automatically_fetch_payment_terms:
if not (
automatically_fetch_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(
@@ -1892,12 +1904,14 @@ class AccountsController(TransactionBase):
reconcilation_entry.party = secondary_party
reconcilation_entry.reference_type = self.doctype
reconcilation_entry.reference_name = self.name
reconcilation_entry.cost_center = self.cost_center
reconcilation_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(
self.company
)
advance_entry.account = primary_account
advance_entry.party_type = primary_party_type
advance_entry.party = primary_party
advance_entry.cost_center = self.cost_center
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes"
if self.doctype == "Sales Invoice":

View File

@@ -576,7 +576,9 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
def get_filtered_dimensions(
doctype, txt, searchfield, start, page_len, filters, reference_doctype=None
):
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
@@ -617,7 +619,12 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
query_filters.append(["name", query_selector, dimensions])
output = frappe.get_list(
doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1
doctype,
fields=fields,
filters=query_filters,
or_filters=or_filters,
as_list=1,
reference_doctype=reference_doctype,
)
return [tuple(d) for d in set(output)]

View File

@@ -329,9 +329,10 @@ class StockController(AccountsController):
"""Create batches if required. Called before submit"""
for d in self.items:
if d.get(warehouse_field) and not d.batch_no:
has_batch_no, create_new_batch = frappe.db.get_value(
has_batch_no, create_new_batch = frappe.get_cached_value(
"Item", d.item_code, ["has_batch_no", "create_new_batch"]
)
if has_batch_no and create_new_batch:
d.batch_no = (
frappe.get_doc(
@@ -414,7 +415,7 @@ class StockController(AccountsController):
"voucher_no": self.name,
"voucher_detail_no": d.name,
"actual_qty": (self.docstatus == 1 and 1 or -1) * flt(d.get("stock_qty")),
"stock_uom": frappe.db.get_value(
"stock_uom": frappe.get_cached_value(
"Item", args.get("item_code") or d.get("item_code"), "stock_uom"
),
"incoming_rate": 0,
@@ -609,7 +610,7 @@ class StockController(AccountsController):
def validate_customer_provided_item(self):
for d in self.get("items"):
# Customer Provided parts will have zero valuation rate
if frappe.db.get_value("Item", d.item_code, "is_customer_provided_item"):
if frappe.get_cached_value("Item", d.item_code, "is_customer_provided_item"):
d.allow_zero_valuation_rate = 1
def set_rate_of_stock_uom(self):
@@ -722,7 +723,7 @@ class StockController(AccountsController):
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
return message
def repost_future_sle_and_gle(self):
def repost_future_sle_and_gle(self, force=False):
args = frappe._dict(
{
"posting_date": self.posting_date,
@@ -733,7 +734,7 @@ class StockController(AccountsController):
}
)
if future_sle_exists(args) or repost_required_for_queue(self):
if force or future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
)
@@ -894,9 +895,6 @@ def future_sle_exists(args, sl_entries=None):
)
for d in data:
if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({})
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data)
@@ -919,7 +917,7 @@ def validate_future_sle_not_exists(args, key, sl_entries=None):
def get_cached_data(args, key):
if key not in frappe.local.future_sle:
return False
frappe.local.future_sle[key] = frappe._dict({})
if args.get("item_code"):
item_key = (args.get("item_code"), args.get("warehouse"))

View File

@@ -976,6 +976,8 @@ def get_itemised_tax_breakup_html(doc):
@frappe.whitelist()
def get_round_off_applicable_accounts(company, account_list):
# required to set correct region
frappe.flags.company = company
account_list = get_regional_round_off_accounts(company, account_list)
return account_list

View File

@@ -60,7 +60,7 @@ class Opportunity(TransactionBase, CRMNote):
if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
self.db_set(field, value)
self.set(field, value)
except Exception:
continue

View File

@@ -370,6 +370,7 @@ scheduler_events = {
"cron": {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
"erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",

View File

@@ -411,7 +411,6 @@ frappe.ui.form.on("BOM", {
}
frm.set_value("process_loss_qty", qty);
frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0);
}
});

View File

@@ -1317,7 +1317,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not field in searchfields
]
query_filters = {"disabled": 0, "end_of_life": (">", today())}
query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}
or_cond_filters = {}
if txt:

View File

@@ -74,6 +74,52 @@ class JobCard(Document):
self.update_sub_operation_status()
self.validate_work_order()
def on_update(self):
self.validate_job_card_qty()
def validate_job_card_qty(self):
if not (self.operation_id and self.work_order):
return
wo_qty = flt(frappe.get_cached_value("Work Order", self.work_order, "qty"))
completed_qty = flt(
frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty")
)
over_production_percentage = flt(
frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")
)
wo_qty = wo_qty + (wo_qty * over_production_percentage / 100)
job_card_qty = frappe.get_all(
"Job Card",
fields=["sum(for_quantity)"],
filters={
"work_order": self.work_order,
"operation_id": self.operation_id,
"docstatus": ["!=", 2],
},
as_list=1,
)
job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
msg = f"""
Qty To Manufacture in the job card
cannot be greater than Qty To Manufacture in the
work order for the operation {bold(self.operation)}.
<br><br><b>Solution: </b> Either you can reduce the
Qty To Manufacture in the job card or set the
'Overproduction Percentage For Work Order'
in the {form_link}."""
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
def set_sub_operations(self):
if not self.sub_operations and self.operation:
self.sub_operations = []

View File

@@ -272,6 +272,42 @@ class TestJobCard(FrappeTestCase):
transfer_entry_2.insert()
self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit)
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0})
def test_job_card_excess_material_transfer_with_no_reference(self):
self.transfer_material_against = "Job Card"
self.source_warehouse = "Stores - _TC"
self.generate_required_stock(self.work_order)
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
row = transfer_entry_1.items[0]
# Add new row without reference of the job card item
transfer_entry_1.append(
"items",
{
"item_code": row.item_code,
"item_name": row.item_name,
"item_group": row.item_group,
"qty": row.qty,
"uom": row.uom,
"conversion_factor": row.conversion_factor,
"stock_uom": row.stock_uom,
"basic_rate": row.basic_rate,
"basic_amount": row.basic_amount,
"expense_account": row.expense_account,
"cost_center": row.cost_center,
"s_warehouse": row.s_warehouse,
"t_warehouse": row.t_warehouse,
},
)
self.assertRaises(frappe.ValidationError, transfer_entry_1.insert)
def test_job_card_partial_material_transfer(self):
"Test partial material transfer against Job Card"
self.transfer_material_against = "Job Card"

View File

@@ -1598,6 +1598,65 @@ class TestWorkOrder(FrappeTestCase):
self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30))
self.assertEqual(row.workstation, workstations_to_check[index])
def test_job_card_extra_qty(self):
items = [
"Test FG Item for Scrap Item Test 1",
"Test RM Item 1 for Scrap Item Test 1",
"Test RM Item 2 for Scrap Item Test 1",
]
company = "_Test Company with perpetual inventory"
for item_code in items:
create_item(
item_code=item_code,
is_stock_item=1,
is_purchase_item=1,
opening_stock=100,
valuation_rate=10,
company=company,
warehouse="Stores - TCP1",
)
item = "Test FG Item for Scrap Item Test 1"
raw_materials = ["Test RM Item 1 for Scrap Item Test 1", "Test RM Item 2 for Scrap Item Test 1"]
if not frappe.db.get_value("BOM", {"item": item}):
bom = make_bom(
item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True
)
bom.with_operations = 1
bom.append(
"operations",
{
"operation": "_Test Operation 1",
"workstation": "_Test Workstation 1",
"hour_rate": 20,
"time_in_mins": 60,
},
)
bom.submit()
wo_order = make_wo_order_test_record(
item=item,
company=company,
planned_start_date=now(),
qty=20,
)
job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name")
job_card_doc = frappe.get_doc("Job Card", job_card)
# Make another Job Card for the same Work Order
job_card2 = frappe.copy_doc(job_card_doc)
self.assertRaises(frappe.ValidationError, job_card2.save)
frappe.db.set_single_value(
"Manufacturing Settings", "overproduction_percentage_for_work_order", 100
)
job_card2 = frappe.copy_doc(job_card_doc)
job_card2.time_logs = []
job_card2.save()
def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation

View File

@@ -625,20 +625,18 @@ erpnext.work_order = {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
}
} else {
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
let allowance_percentage = doc.overproduction_percentage_for_work_order;
} else if (frm.doc.__onload && frm.doc.__onload.overproduction_percentage) {
let allowance_percentage = frm.doc.__onload.overproduction_percentage;
if (allowance_percentage > 0) {
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
if (allowance_percentage > 0) {
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
if ((flt(doc.produced_qty) < allowed_qty)) {
frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
}
if ((flt(doc.produced_qty) < allowed_qty)) {
frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
}
});
}
}
}
} else {

View File

@@ -69,7 +69,7 @@ def get_columns(filters):
"label": _("Id"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Work Order",
"options": "Quality Inspection",
"width": 100,
},
{"label": _("Report Date"), "fieldname": "report_date", "fieldtype": "Date", "width": 150},

View File

@@ -326,5 +326,7 @@ erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v14_0.set_pick_list_status
erpnext.patches.v13_0.update_docs_link
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
# below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.patches.v14_0.update_company_in_ldc

View File

@@ -0,0 +1,14 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from erpnext import get_default_company
def execute():
company = get_default_company()
if company:
for d in frappe.get_all("Lower Deduction Certificate", pluck="name"):
frappe.db.set_value("Lower Deduction Certificate", d, "company", company, update_modified=False)

View File

@@ -10,62 +10,6 @@ from frappe.website.serve import get_response
class TestHomepageSection(unittest.TestCase):
def test_homepage_section_card(self):
try:
frappe.get_doc(
{
"doctype": "Homepage Section",
"name": "Card Section",
"section_based_on": "Cards",
"section_cards": [
{
"title": "Card 1",
"subtitle": "Subtitle 1",
"content": "This is test card 1",
"route": "/card-1",
},
{
"title": "Card 2",
"subtitle": "Subtitle 2",
"content": "This is test card 2",
"image": "test.jpg",
},
],
"no_of_columns": 3,
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
set_request(method="GET", path="home")
response = get_response()
self.assertEqual(response.status_code, 200)
html = frappe.safe_decode(response.get_data())
soup = BeautifulSoup(html, "html.parser")
sections = soup.find("main").find_all("section")
self.assertEqual(len(sections), 3)
homepage_section = sections[2]
self.assertEqual(homepage_section.h3.text, "Card Section")
cards = homepage_section.find_all(class_="card")
self.assertEqual(len(cards), 2)
self.assertEqual(cards[0].h5.text, "Card 1")
self.assertEqual(cards[0].a["href"], "/card-1")
self.assertEqual(cards[1].p.text, "Subtitle 2")
img = cards[1].find(class_="card-img-top")
self.assertEqual(img["src"], "test.jpg")
self.assertEqual(img["loading"], "lazy")
# cleanup
frappe.db.rollback()
def test_homepage_section_custom_html(self):
frappe.get_doc(
{

View File

@@ -96,7 +96,6 @@
"read_only": 1
},
{
"depends_on": "eval:!doc.work_order || doc.docstatus == 1",
"fieldname": "employee_detail",
"fieldtype": "Section Break",
"label": "Employee Detail"
@@ -311,7 +310,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-02-14 04:55:41.735991",
"modified": "2023-04-20 15:59:11.107831",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",

View File

@@ -68,7 +68,7 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) {
// New activity if no activities found
var args = dialog.get_values();
if(!args) return;
if (frm.doc.time_logs.length <= 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) {
if (frm.doc.time_logs.length == 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) {
frm.doc.time_logs = [];
}
row = frappe.model.add_child(frm.doc, "Timesheet Detail", "time_logs");

View File

@@ -13,19 +13,11 @@ frappe.setup.on("before_load", function () {
erpnext.setup.slides_settings = [
{
// Brand
name: 'brand',
icon: "fa fa-bookmark",
title: __("The Brand"),
// help: __('Upload your letter head and logo. (you can edit them later).'),
// Organization
name: 'organization',
title: __("Setup your organization"),
icon: "fa fa-building",
fields: [
{
fieldtype: "Attach Image", fieldname: "attach_logo",
label: __("Attach Logo"),
description: __("100px by 100px"),
is_private: 0,
align: 'center'
},
{
fieldname: 'company_name',
label: __('Company Name'),
@@ -35,54 +27,9 @@ erpnext.setup.slides_settings = [
{
fieldname: 'company_abbr',
label: __('Company Abbreviation'),
fieldtype: 'Data'
}
],
onload: function(slide) {
this.bind_events(slide);
},
bind_events: function (slide) {
slide.get_input("company_name").on("change", function () {
var parts = slide.get_input("company_name").val().split(" ");
var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
slide.get_input("company_abbr").on("change", function () {
if (slide.get_input("company_abbr").val().length > 10) {
frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
slide.get_field("company_abbr").set_value("");
}
});
},
validate: function() {
if ((this.values.company_name || "").toLowerCase() == "company") {
frappe.msgprint(__("Company Name cannot be Company"));
return false;
}
if (!this.values.company_abbr) {
return false;
}
if (this.values.company_abbr.length > 10) {
return false;
}
return true;
}
},
{
// Organisation
name: 'organisation',
title: __("Your Organization"),
icon: "fa fa-building",
fields: [
{
fieldname: 'company_tagline',
label: __('What does it do?'),
fieldtype: 'Data',
placeholder: __('e.g. "Build tools for builders"'),
reqd: 1
hidden: 1
},
{ fieldname: 'bank_account', label: __('Bank Name'), fieldtype: 'Data', reqd: 1 },
{
fieldname: 'chart_of_accounts', label: __('Chart of Accounts'),
options: "", fieldtype: 'Select'
@@ -94,40 +41,24 @@ erpnext.setup.slides_settings = [
],
onload: function (slide) {
this.load_chart_of_accounts(slide);
this.bind_events(slide);
this.load_chart_of_accounts(slide);
this.set_fy_dates(slide);
},
validate: function () {
let me = this;
let exist;
if (!this.validate_fy_dates()) {
return false;
}
// Validate bank name
if(me.values.bank_account) {
frappe.call({
async: false,
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",
args: {
"coa": me.values.chart_of_accounts,
"bank_account": me.values.bank_account
},
callback: function (r) {
if(r.message){
exist = r.message;
me.get_field("bank_account").set_value("");
let message = __('Account {0} already exists. Please enter a different name for your bank account.',
[me.values.bank_account]
);
frappe.msgprint(message);
}
}
});
return !exist; // Return False if exist = true
if ((this.values.company_name || "").toLowerCase() == "company") {
frappe.msgprint(__("Company Name cannot be Company"));
return false;
}
if (!this.values.company_abbr) {
return false;
}
if (this.values.company_abbr.length > 10) {
return false;
}
return true;
@@ -151,15 +82,15 @@ erpnext.setup.slides_settings = [
var country = frappe.wizard.values.country;
if (country) {
var fy = erpnext.setup.fiscal_years[country];
var current_year = moment(new Date()).year();
var next_year = current_year + 1;
let fy = erpnext.setup.fiscal_years[country];
let current_year = moment(new Date()).year();
let next_year = current_year + 1;
if (!fy) {
fy = ["01-01", "12-31"];
next_year = current_year;
}
var year_start_date = current_year + "-" + fy[0];
let year_start_date = current_year + "-" + fy[0];
if (year_start_date > frappe.datetime.get_today()) {
next_year = current_year;
current_year -= 1;
@@ -171,7 +102,7 @@ erpnext.setup.slides_settings = [
load_chart_of_accounts: function (slide) {
var country = frappe.wizard.values.country;
let country = frappe.wizard.values.country;
if (country) {
frappe.call({
@@ -202,12 +133,25 @@ erpnext.setup.slides_settings = [
me.charts_modal(slide, chart_template);
});
slide.get_input("company_name").on("change", function () {
let parts = slide.get_input("company_name").val().split(" ");
let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
slide.get_input("company_abbr").on("change", function () {
if (slide.get_input("company_abbr").val().length > 10) {
frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
slide.get_field("company_abbr").set_value("");
}
});
},
charts_modal: function(slide, chart_template) {
let parent = __('All Accounts');
var dialog = new frappe.ui.Dialog({
let dialog = new frappe.ui.Dialog({
title: chart_template,
fields: [
{'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button',

View File

@@ -10,6 +10,7 @@
"tax_withholding_category",
"fiscal_year",
"column_break_3",
"company",
"certificate_no",
"section_break_3",
"supplier",
@@ -123,11 +124,18 @@
"label": "Tax Withholding Category",
"options": "Tax Withholding Category",
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-10-23 18:33:38.962622",
"modified": "2023-04-18 08:25:35.302081",
"modified_by": "Administrator",
"module": "Regional",
"name": "Lower Deduction Certificate",
@@ -136,5 +144,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,31 +1,135 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import re
import frappe
from frappe import _
from frappe.utils import format_datetime
COLUMNS = [
{
"label": "JournalCode",
"fieldname": "JournalCode",
"fieldtype": "Data",
"width": 90,
},
{
"label": "JournalLib",
"fieldname": "JournalLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureNum",
"fieldname": "EcritureNum",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureDate",
"fieldname": "EcritureDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "CompteNum",
"fieldname": "CompteNum",
"fieldtype": "Link",
"options": "Account",
"width": 100,
},
{
"label": "CompteLib",
"fieldname": "CompteLib",
"fieldtype": "Link",
"options": "Account",
"width": 200,
},
{
"label": "CompAuxNum",
"fieldname": "CompAuxNum",
"fieldtype": "Data",
"width": 90,
},
{
"label": "CompAuxLib",
"fieldname": "CompAuxLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "PieceRef",
"fieldname": "PieceRef",
"fieldtype": "Data",
"width": 90,
},
{
"label": "PieceDate",
"fieldname": "PieceDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureLib",
"fieldname": "EcritureLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Debit",
"fieldname": "Debit",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Credit",
"fieldname": "Credit",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureLet",
"fieldname": "EcritureLet",
"fieldtype": "Data",
"width": 90,
},
{
"label": "DateLet",
"fieldname": "DateLet",
"fieldtype": "Data",
"width": 90,
},
{
"label": "ValidDate",
"fieldname": "ValidDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Montantdevise",
"fieldname": "Montantdevise",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Idevise",
"fieldname": "Idevise",
"fieldtype": "Data",
"width": 90,
},
]
def execute(filters=None):
account_details = {}
for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1):
account_details.setdefault(acc.name, acc)
validate_filters(filters, account_details)
filters = set_account_currency(filters)
columns = get_columns(filters)
res = get_result(filters)
return columns, res
validate_filters(filters)
return COLUMNS, get_result(
company=filters["company"],
fiscal_year=filters["fiscal_year"],
)
def validate_filters(filters, account_details):
def validate_filters(filters):
if not filters.get("company"):
frappe.throw(_("{0} is mandatory").format(_("Company")))
@@ -33,107 +137,96 @@ def validate_filters(filters, account_details):
frappe.throw(_("{0} is mandatory").format(_("Fiscal Year")))
def set_account_currency(filters):
def get_gl_entries(company, fiscal_year):
gle = frappe.qb.DocType("GL Entry")
sales_invoice = frappe.qb.DocType("Sales Invoice")
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
journal_entry = frappe.qb.DocType("Journal Entry")
payment_entry = frappe.qb.DocType("Payment Entry")
customer = frappe.qb.DocType("Customer")
supplier = frappe.qb.DocType("Supplier")
employee = frappe.qb.DocType("Employee")
filters["company_currency"] = frappe.get_cached_value(
"Company", filters.company, "default_currency"
debit = frappe.query_builder.functions.Sum(gle.debit).as_("debit")
credit = frappe.query_builder.functions.Sum(gle.credit).as_("credit")
debit_currency = frappe.query_builder.functions.Sum(gle.debit_in_account_currency).as_(
"debitCurr"
)
credit_currency = frappe.query_builder.functions.Sum(gle.credit_in_account_currency).as_(
"creditCurr"
)
return filters
def get_columns(filters):
columns = [
"JournalCode" + "::90",
"JournalLib" + "::90",
"EcritureNum" + ":Dynamic Link:90",
"EcritureDate" + "::90",
"CompteNum" + ":Link/Account:100",
"CompteLib" + ":Link/Account:200",
"CompAuxNum" + "::90",
"CompAuxLib" + "::90",
"PieceRef" + "::90",
"PieceDate" + "::90",
"EcritureLib" + "::90",
"Debit" + "::90",
"Credit" + "::90",
"EcritureLet" + "::90",
"DateLet" + "::90",
"ValidDate" + "::90",
"Montantdevise" + "::90",
"Idevise" + "::90",
]
return columns
def get_result(filters):
gl_entries = get_gl_entries(filters)
result = get_result_as_list(gl_entries, filters)
return result
def get_gl_entries(filters):
group_by_condition = (
"group by voucher_type, voucher_no, account"
if filters.get("group_by_voucher")
else "group by gl.name"
query = (
frappe.qb.from_(gle)
.left_join(sales_invoice)
.on(gle.voucher_no == sales_invoice.name)
.left_join(purchase_invoice)
.on(gle.voucher_no == purchase_invoice.name)
.left_join(journal_entry)
.on(gle.voucher_no == journal_entry.name)
.left_join(payment_entry)
.on(gle.voucher_no == payment_entry.name)
.left_join(customer)
.on(gle.party == customer.name)
.left_join(supplier)
.on(gle.party == supplier.name)
.left_join(employee)
.on(gle.party == employee.name)
.select(
gle.posting_date.as_("GlPostDate"),
gle.name.as_("GlName"),
gle.account,
gle.transaction_date,
debit,
credit,
debit_currency,
credit_currency,
gle.voucher_type,
gle.voucher_no,
gle.against_voucher_type,
gle.against_voucher,
gle.account_currency,
gle.against,
gle.party_type,
gle.party,
sales_invoice.name.as_("InvName"),
sales_invoice.title.as_("InvTitle"),
sales_invoice.posting_date.as_("InvPostDate"),
purchase_invoice.name.as_("PurName"),
purchase_invoice.title.as_("PurTitle"),
purchase_invoice.posting_date.as_("PurPostDate"),
journal_entry.cheque_no.as_("JnlRef"),
journal_entry.posting_date.as_("JnlPostDate"),
journal_entry.title.as_("JnlTitle"),
payment_entry.name.as_("PayName"),
payment_entry.posting_date.as_("PayPostDate"),
payment_entry.title.as_("PayTitle"),
customer.customer_name,
customer.name.as_("cusName"),
supplier.supplier_name,
supplier.name.as_("supName"),
employee.employee_name,
employee.name.as_("empName"),
)
.where((gle.company == company) & (gle.fiscal_year == fiscal_year))
.groupby(gle.voucher_type, gle.voucher_no, gle.account)
.orderby(gle.posting_date, gle.voucher_no)
)
gl_entries = frappe.db.sql(
"""
select
gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date,
sum(gl.debit) as debit, sum(gl.credit) as credit,
sum(gl.debit_in_account_currency) as debitCurr, sum(gl.credit_in_account_currency) as creditCurr,
gl.voucher_type, gl.voucher_no, gl.against_voucher_type,
gl.against_voucher, gl.account_currency, gl.against,
gl.party_type, gl.party,
inv.name as InvName, inv.title as InvTitle, inv.posting_date as InvPostDate,
pur.name as PurName, pur.title as PurTitle, pur.posting_date as PurPostDate,
jnl.cheque_no as JnlRef, jnl.posting_date as JnlPostDate, jnl.title as JnlTitle,
pay.name as PayName, pay.posting_date as PayPostDate, pay.title as PayTitle,
cus.customer_name, cus.name as cusName,
sup.supplier_name, sup.name as supName,
emp.employee_name, emp.name as empName,
stu.title as student_name, stu.name as stuName,
member_name, mem.name as memName
from `tabGL Entry` gl
left join `tabSales Invoice` inv on gl.voucher_no = inv.name
left join `tabPurchase Invoice` pur on gl.voucher_no = pur.name
left join `tabJournal Entry` jnl on gl.voucher_no = jnl.name
left join `tabPayment Entry` pay on gl.voucher_no = pay.name
left join `tabCustomer` cus on gl.party = cus.name
left join `tabSupplier` sup on gl.party = sup.name
left join `tabEmployee` emp on gl.party = emp.name
left join `tabStudent` stu on gl.party = stu.name
left join `tabMember` mem on gl.party = mem.name
where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s
{group_by_condition}
order by GlPostDate, voucher_no""".format(
group_by_condition=group_by_condition
),
filters,
as_dict=1,
)
return gl_entries
return query.run(as_dict=True)
def get_result_as_list(data, filters):
def get_result(company, fiscal_year):
data = get_gl_entries(company, fiscal_year)
result = []
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
company_currency = frappe.get_cached_value("Company", company, "default_currency")
accounts = frappe.get_all(
"Account", filters={"Company": filters.company}, fields=["name", "account_number"]
"Account", filters={"Company": company}, fields=["name", "account_number"]
)
for d in data:
JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0]
if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith(
@@ -141,9 +234,7 @@ def get_result_as_list(data, filters):
):
EcritureNum = re.split("-|/", d.get("voucher_no"))[1]
else:
EcritureNum = re.search(
r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE
).group(1)
EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE)[1]
EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
@@ -185,7 +276,7 @@ def get_result_as_list(data, filters):
ValidDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
PieceRef = d.get("voucher_no") if d.get("voucher_no") else "Sans Reference"
PieceRef = d.get("voucher_no") or "Sans Reference"
# EcritureLib is the reference title unless it is an opening entry
if d.get("is_opening") == "Yes":

View File

@@ -30,10 +30,6 @@
"cost_center",
"dimension_col_break",
"project",
"column_break_77",
"source",
"campaign",
"custom_dimensions_section",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -162,7 +158,9 @@
"is_internal_customer",
"represents_company",
"column_break_152",
"source",
"inter_company_order_reference",
"campaign",
"party_account_currency",
"connections_tab"
],
@@ -1164,12 +1162,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_77",
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{
"fieldname": "source",
"fieldtype": "Link",
@@ -1612,10 +1604,6 @@
"fieldname": "column_break_92",
"fieldtype": "Column Break"
},
{
"fieldname": "custom_dimensions_section",
"fieldtype": "Section Break"
},
{
"collapsible": 1,
"fieldname": "additional_info_section",
@@ -1643,7 +1631,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2022-12-12 18:34:00.681780",
"modified": "2023-04-20 11:14:01.036202",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -547,7 +547,7 @@ def make_material_request(source_name, target_doc=None):
# qty is for packed items, because packed items don't have stock_qty field
qty = source.get("qty")
target.project = source_parent.project
target.qty = qty - requested_item_qty.get(source.name, 0)
target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
args = target.as_dict().copy()
@@ -581,7 +581,7 @@ def make_material_request(source_name, target_doc=None):
"doctype": "Material Request Item",
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
and doc.stock_qty > requested_item_qty.get(doc.name, 0),
and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0),
"postprocess": update_item,
},
},
@@ -1340,8 +1340,9 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
.select(Sum(wo.qty))
.where(
(wo.production_item == i.item_code)
& (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
& (wo.docstatus.lte(2))
& (wo.sales_order == so.name)
& (wo.sales_order_item == i.name)
& (wo.docstatus.lt(2))
)
.run()[0][0]
)

View File

@@ -57,7 +57,7 @@ frappe.listview_settings['Sales Order'] = {
});
listview.page.add_action_item(__("Advance Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Advance Payment");
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry");
});
}

View File

@@ -1878,6 +1878,37 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(pe.references[1].reference_name, so.name)
self.assertEqual(pe.references[1].allocated_amount, 300)
def test_delivered_item_material_request(self):
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_se_from_wo,
)
from erpnext.stock.doctype.material_request.material_request import raise_work_orders
so = make_sales_order(
item_list=[
{"item_code": "_Test FG Item", "qty": 10, "rate": 100, "warehouse": "Work In Progress - _TC"}
]
)
make_stock_entry(
item_code="_Test FG Item", target="Work In Progress - _TC", qty=4, basic_rate=100
)
dn = make_delivery_note(so.name)
dn.items[0].qty = 4
dn.submit()
so.load_from_db()
self.assertEqual(so.items[0].delivered_qty, 4)
mr = make_material_request(so.name)
mr.material_request_type = "Purchase"
mr.schedule_date = today()
mr.save()
self.assertEqual(mr.items[0].qty, 6)
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")

View File

@@ -559,8 +559,10 @@ erpnext.PointOfSale.Controller = class {
item_row = this.frm.add_child('items', new_item);
if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
if (field === 'qty' && value !== 0 && !this.allow_negative_stock) {
const qty_needed = value * item_row.conversion_factor;
await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
}
await this.trigger_new_item_events(item_row);

View File

@@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.utils import cstr, getdate
from .default_website import website_maker
def create_fiscal_year_and_company(args):
@@ -48,83 +47,6 @@ def enable_shopping_cart(args): # nosemgrep
).insert()
def create_email_digest():
from frappe.utils.user import get_system_managers
system_managers = get_system_managers(only_name=True)
if not system_managers:
return
recipients = []
for d in system_managers:
recipients.append({"recipient": d})
companies = frappe.db.sql_list("select name FROM `tabCompany`")
for company in companies:
if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company):
edigest = frappe.get_doc(
{
"doctype": "Email Digest",
"name": "Default Weekly Digest - " + company,
"company": company,
"frequency": "Weekly",
"recipients": recipients,
}
)
for df in edigest.meta.get("fields", {"fieldtype": "Check"}):
if df.fieldname != "scheduler_errors":
edigest.set(df.fieldname, 1)
edigest.insert()
# scheduler errors digest
if companies:
edigest = frappe.new_doc("Email Digest")
edigest.update(
{
"name": "Scheduler Errors",
"company": companies[0],
"frequency": "Daily",
"recipients": recipients,
"scheduler_errors": 1,
"enabled": 1,
}
)
edigest.insert()
def create_logo(args):
if args.get("attach_logo"):
attach_logo = args.get("attach_logo").split(",")
if len(attach_logo) == 3:
filename, filetype, content = attach_logo
_file = frappe.get_doc(
{
"doctype": "File",
"file_name": filename,
"attached_to_doctype": "Website Settings",
"attached_to_name": "Website Settings",
"decode": True,
}
)
_file.save()
fileurl = _file.file_url
frappe.db.set_value(
"Website Settings",
"Website Settings",
"brand_html",
"<img src='{0}' style='max-width: 40px; max-height: 25px;'> {1}".format(
fileurl, args.get("company_name")
),
)
def create_website(args):
website_maker(args)
def get_fy_details(fy_start_date, fy_end_date):
start_year = getdate(fy_start_date).year
if start_year == getdate(fy_end_date).year:

View File

@@ -1,89 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.utils import nowdate
class website_maker(object):
def __init__(self, args):
self.args = args
self.company = args.company_name
self.tagline = args.company_tagline
self.user = args.get("email")
self.make_web_page()
self.make_website_settings()
self.make_blog()
def make_web_page(self):
# home page
homepage = frappe.get_doc("Homepage", "Homepage")
homepage.company = self.company
homepage.tag_line = self.tagline
homepage.setup_items()
homepage.save()
def make_website_settings(self):
# update in home page in settings
website_settings = frappe.get_doc("Website Settings", "Website Settings")
website_settings.home_page = "home"
website_settings.brand_html = self.company
website_settings.copyright = self.company
website_settings.top_bar_items = []
website_settings.append(
"top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"}
)
website_settings.append(
"top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"}
)
website_settings.append(
"top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"}
)
website_settings.save()
def make_blog(self):
blog_category = frappe.get_doc(
{"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")}
).insert()
if not self.user:
# Admin setup
return
blogger = frappe.new_doc("Blogger")
user = frappe.get_doc("User", self.user)
blogger.user = self.user
blogger.full_name = user.first_name + (" " + user.last_name if user.last_name else "")
blogger.short_name = user.first_name.lower()
blogger.avatar = user.user_image
blogger.insert()
frappe.get_doc(
{
"doctype": "Blog Post",
"title": "Welcome",
"published": 1,
"published_on": nowdate(),
"blogger": blogger.name,
"blog_category": blog_category.name,
"blog_intro": "My First Blog",
"content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(),
}
).insert()
def test():
frappe.delete_doc("Web Page", "test-company")
frappe.delete_doc("Blog Post", "welcome")
frappe.delete_doc("Blogger", "administrator")
frappe.delete_doc("Blog Category", "general")
website_maker(
{
"company": "Test Company",
"company_tagline": "Better Tools for Everyone",
"name": "Administrator",
}
)
frappe.db.commit()

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from .operations import company_setup
from .operations import install_fixtures as fixtures
@@ -35,7 +34,6 @@ def get_setup_stages(args=None):
"fail_msg": "Failed to set defaults",
"tasks": [
{"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")},
{"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")},
],
},
{
@@ -60,12 +58,6 @@ def setup_defaults(args):
fixtures.install_defaults(frappe._dict(args))
def stage_four(args):
company_setup.create_website(args)
company_setup.create_email_digest()
company_setup.create_logo(args)
def fin(args):
frappe.local.message_log = []
login_as_first_user(args)
@@ -81,5 +73,4 @@ def setup_complete(args=None):
stage_fixtures(args)
setup_company(args)
setup_defaults(args)
stage_four(args)
fin(args)

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.query_builder.functions import CurDate, Sum, Timestamp
from frappe.query_builder.functions import CombineDatetime, CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form, nowtime
from frappe.utils.data import add_days
from frappe.utils.jinja import render_template
@@ -192,7 +192,8 @@ def get_batch_qty(
posting_time = nowtime()
query = query.where(
Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time)
CombineDatetime(sle.posting_date, sle.posting_time)
<= CombineDatetime(posting_date, posting_time)
)
out = query.run(as_list=True)[0][0] or 0
@@ -376,7 +377,7 @@ def get_pos_reserved_batch_qty(filters):
p = frappe.qb.DocType("POS Invoice").as_("p")
item = frappe.qb.DocType("POS Invoice Item").as_("item")
sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty")
sum_qty = frappe.query_builder.functions.Sum(item.stock_qty).as_("qty")
reserved_batch_qty = (
frappe.qb.from_(p)

View File

@@ -28,8 +28,6 @@
"column_break_18",
"project",
"dimension_col_break",
"campaign",
"source",
"custom_dimensions_section",
"currency_and_price_list",
"currency",
@@ -161,11 +159,12 @@
"inter_company_reference",
"customer_group",
"territory",
"source",
"campaign",
"column_break5",
"excise_page",
"instructions",
"connections_tab",
"column_break_25"
"connections_tab"
],
"fields": [
{
@@ -1339,10 +1338,6 @@
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_30",
"fieldtype": "Section Break",
@@ -1403,7 +1398,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2023-02-14 04:45:44.179670",
"modified": "2023-04-21 11:15:23.931084",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -118,7 +118,7 @@ class DeliveryNote(SellingController):
def so_required(self):
"""check in manage account if sales order required or not"""
if frappe.db.get_value("Selling Settings", None, "so_required") == "Yes":
if frappe.db.get_single_value("Selling Settings", "so_required") == "Yes":
for d in self.get("items"):
if not d.against_sales_order:
frappe.throw(_("Sales Order required for Item {0}").format(d.item_code))
@@ -205,7 +205,7 @@ class DeliveryNote(SellingController):
super(DeliveryNote, self).validate_warehouse()
for d in self.get_item_list():
if not d["warehouse"] and frappe.db.get_value("Item", d["item_code"], "is_stock_item") == 1:
if not d["warehouse"] and frappe.get_cached_value("Item", d["item_code"], "is_stock_item") == 1:
frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"]))
def update_current_stock(self):

View File

@@ -629,7 +629,8 @@
"no_copy": 1,
"options": "Sales Order",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "against_sales_invoice",
@@ -662,7 +663,8 @@
"label": "Against Sales Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "installed_qty",
@@ -854,7 +856,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-04-06 09:28:29.182053",
"modified": "2023-05-01 21:05:14.175640",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

@@ -619,7 +619,7 @@ def make_stock_entry(source_name, target_doc=None):
target.set_transfer_qty()
target.set_actual_qty()
target.calculate_rate_and_amount(raise_error_if_no_rate=False)
target.set_stock_entry_type()
target.stock_entry_type = target.purpose
target.set_job_card_data()
doclist = get_mapped_doc(

View File

@@ -54,6 +54,8 @@ class TestMaterialRequest(FrappeTestCase):
mr.submit()
se = make_stock_entry(mr.name)
self.assertEqual(se.stock_entry_type, "Material Transfer")
self.assertEqual(se.purpose, "Material Transfer")
self.assertEqual(se.doctype, "Stock Entry")
self.assertEqual(len(se.get("items")), len(mr.get("items")))
@@ -69,6 +71,8 @@ class TestMaterialRequest(FrappeTestCase):
in_transit_warehouse = get_in_transit_warehouse(mr.company)
se = make_in_transit_stock_entry(mr.name, in_transit_warehouse)
self.assertEqual(se.stock_entry_type, "Material Transfer")
self.assertEqual(se.purpose, "Material Transfer")
self.assertEqual(se.doctype, "Stock Entry")
for row in se.get("items"):
self.assertEqual(row.t_warehouse, in_transit_warehouse)

View File

@@ -379,8 +379,20 @@ class PurchaseReceipt(BuyingController):
)
outgoing_amount = d.base_net_amount
if self.is_internal_supplier and d.valuation_rate:
outgoing_amount = d.valuation_rate * d.stock_qty
if self.is_internal_transfer() and d.valuation_rate:
outgoing_amount = abs(
frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": self.name,
"voucher_detail_no": d.name,
"warehouse": d.from_warehouse,
"is_cancelled": 0,
},
"stock_value_difference",
)
)
credit_amount = outgoing_amount
if credit_amount:

View File

@@ -1610,6 +1610,147 @@ class TestPurchaseReceipt(FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
def test_internal_pr_gl_entries(self):
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100})
make_stock_entry(
purpose="Material Receipt",
item_code=item.name,
qty=10,
company=company,
to_warehouse=from_warehouse,
posting_date=add_days(today(), -3),
)
# Step - 1: Create Delivery Note with Internal Customer
dn = create_delivery_note(
item_code=item.name,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=10,
rate=100,
warehouse=from_warehouse,
target_warehouse=target_warehouse,
posting_date=add_days(today(), -2),
)
# Step - 2: Create Internal Purchase Receipt
pr = make_inter_company_purchase_receipt(dn.name)
pr.items[0].qty = 10
pr.items[0].from_warehouse = target_warehouse
pr.items[0].warehouse = to_warehouse
pr.items[0].rejected_warehouse = from_warehouse
pr.save()
pr.submit()
# Step - 3: Create back-date Stock Reconciliation [After DN and Before PR]
create_stock_reconciliation(
item_code=item.name,
warehouse=target_warehouse,
qty=10,
rate=50,
company=company,
posting_date=add_days(today(), -1),
)
warehouse_account = get_warehouse_account_map(company)
stock_account_value = frappe.db.get_value(
"GL Entry",
{
"account": warehouse_account[target_warehouse]["account"],
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"is_cancelled": 0,
},
fieldname=["credit"],
)
stock_diff = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"is_cancelled": 0,
},
fieldname=["sum(stock_value_difference)"],
)
# Value of Stock Account should be equal to the sum of Stock Value Difference
self.assertEqual(stock_account_value, stock_diff)
def test_internal_pr_reference(self):
item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100})
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
from_warehouse = create_warehouse("_Test Internal From Warehouse New 1", company=company)
target_warehouse = create_warehouse("_Test Internal GIT Warehouse New 1", company=company)
to_warehouse = create_warehouse("_Test Internal To Warehouse New 1", company=company)
# Step 2: Create Stock Entry (Material Receipt)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
make_stock_entry(
purpose="Material Receipt",
item_code=item.name,
qty=15,
company=company,
to_warehouse=from_warehouse,
)
# Step 3: Create Delivery Note with Internal Customer
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
dn = create_delivery_note(
item_code=item.name,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=10,
rate=100,
warehouse=from_warehouse,
target_warehouse=target_warehouse,
)
# Step 4: Create Internal Purchase Receipt
from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
pr = make_inter_company_purchase_receipt(dn.name)
pr.inter_company_reference = ""
self.assertRaises(frappe.ValidationError, pr.save)
pr.inter_company_reference = dn.name
pr.items[0].qty = 10
pr.items[0].from_warehouse = target_warehouse
pr.items[0].warehouse = to_warehouse
pr.items[0].rejected_warehouse = from_warehouse
pr.save()
delivery_note_item = pr.items[0].delivery_note_item
pr.items[0].delivery_note_item = ""
self.assertRaises(frappe.ValidationError, pr.save)
pr.load_from_db()
pr.items[0].delivery_note_item = delivery_note_item
pr.save()
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -6,7 +6,7 @@ from frappe import _
from frappe.exceptions import QueryDeadlockError, QueryTimeoutError
from frappe.model.document import Document
from frappe.query_builder import DocType, Interval
from frappe.query_builder.functions import Now
from frappe.query_builder.functions import Max, Now
from frappe.utils import cint, get_link_to_form, get_weekday, getdate, now, nowtime
from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
@@ -36,11 +36,38 @@ class RepostItemValuation(Document):
)
def validate(self):
self.validate_period_closing_voucher()
self.set_status(write=False)
self.reset_field_values()
self.set_company()
self.validate_accounts_freeze()
def validate_period_closing_voucher(self):
year_end_date = self.get_max_year_end_date(self.company)
if year_end_date and getdate(self.posting_date) <= getdate(year_end_date):
msg = f"Due to period closing, you cannot repost item valuation before {year_end_date}"
frappe.throw(_(msg))
@staticmethod
def get_max_year_end_date(company):
data = frappe.get_all(
"Period Closing Voucher", fields=["fiscal_year"], filters={"docstatus": 1, "company": company}
)
if not data:
return
fiscal_years = [d.fiscal_year for d in data]
table = frappe.qb.DocType("Fiscal Year")
query = (
frappe.qb.from_(table)
.select(Max(table.year_end_date))
.where((table.name.isin(fiscal_years)) & (table.disabled == 0))
).run()
return query[0][0] if query else None
def validate_accounts_freeze(self):
acc_settings = frappe.db.get_value(
"Accounts Settings",

View File

@@ -130,6 +130,7 @@ class StockEntry(StockController):
self.validate_fg_completed_qty()
self.validate_difference_account()
self.set_job_card_data()
self.validate_job_card_item()
self.set_purpose_for_stock_entry()
self.clean_serial_nos()
self.validate_duplicate_serial_no()
@@ -214,6 +215,24 @@ class StockEntry(StockController):
self.from_bom = 1
self.bom_no = data.bom_no
def validate_job_card_item(self):
if not self.job_card:
return
if cint(frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")):
return
for row in self.items:
if row.job_card_item:
continue
msg = f"""Row #{0}: The job card item reference
is missing. Kindly create the stock entry
from the job card. If you have added the row manually
then you won't be able to add job card item reference."""
frappe.throw(_(msg))
def validate_work_order_status(self):
pro_doc = frappe.get_doc("Work Order", self.work_order)
if pro_doc.status == "Completed":
@@ -2349,7 +2368,7 @@ def move_sample_to_retention_warehouse(company, items):
@frappe.whitelist()
def make_stock_in_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.set_stock_entry_type()
target.stock_entry_type = "Material Transfer"
target.set_missing_values()
def update_item(source_doc, target_doc, source_parent):

View File

@@ -202,6 +202,9 @@ class TestStockEntry(FrappeTestCase):
)
end_transit_entry = make_stock_in_entry(transit_entry.name)
self.assertEqual(end_transit_entry.stock_entry_type, "Material Transfer")
self.assertEqual(end_transit_entry.purpose, "Material Transfer")
self.assertEqual(transit_entry.name, end_transit_entry.outgoing_stock_entry)
self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry)
self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail)

View File

@@ -530,7 +530,9 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
# check if cancellation of stock reco is blocked
self.assertRaises(NegativeStockError, sr.cancel)
repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name}))
repost_exists = bool(
frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name, "status": "Queued"})
)
self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation")
def test_intermediate_sr_bin_update(self):

View File

@@ -1508,11 +1508,15 @@ def get_so_reservation_for_item(args):
elif args.get("against_sales_invoice"):
sales_order = frappe.db.get_all(
"Sales Invoice Item",
filters={"parent": args.get("against_sales_invoice"), "item_code": args.get("item_code")},
filters={
"parent": args.get("against_sales_invoice"),
"item_code": args.get("item_code"),
"docstatus": 1,
},
fields="sales_order",
)
if sales_order and sales_order[0]:
if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")):
if get_reserved_qty_for_so(sales_order[0].sales_order, args.get("item_code")):
reserved_so = sales_order[0]
elif args.get("sales_order"):
if get_reserved_qty_for_so(args.get("sales_order"), args.get("item_code")):

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